Changed interface; Cosmetics
[privoxy.git] / pcrs.c
1 const char pcrs_rcs[] = "$Id: pcrs.c,v 1.8 2001/06/29 21:45:41 oes Exp $";
2
3 /*********************************************************************
4  *
5  * File        :  $Source: /cvsroot/ijbswa/current/pcrs.c,v $
6  *
7  * Purpose     :  pcrs is a supplement to the brilliant pcre library by Philip
8  *                Hazel (ph10@cam.ac.uk) and adds Perl-style substitution. That
9  *                is, it mimics Perl's 's' operator.
10  *
11  *                Currently, there's no documentation besides comments and the
12  *                source itself ;-)
13  *
14  *                Note: In addition to perl's options, 'U' for ungreedy and 'T'
15  *                for trivial (i.e.: ignore backrefs in the substitute) are
16  *                supported.
17  *
18  * Copyright   :  Written and Copyright (C) 2000, 2001 by Andreas S. Oesterhelt
19  *                <andreas@oesterhelt.org>
20  *
21  *                This program is free software; you can redistribute it 
22  *                and/or modify it under the terms of the GNU General
23  *                Public License as published by the Free Software
24  *                Foundation; either version 2 of the License, or (at
25  *                your option) any later version.
26  *
27  *                This program is distributed in the hope that it will
28  *                be useful, but WITHOUT ANY WARRANTY; without even the
29  *                implied warranty of MERCHANTABILITY or FITNESS FOR A
30  *                PARTICULAR PURPOSE.  See the GNU General Public
31  *                License for more details.
32  *
33  *                The GNU General Public License should be included with
34  *                this file.  If not, you can view it at
35  *                http://www.gnu.org/copyleft/gpl.html
36  *                or write to the Free Software Foundation, Inc., 59
37  *                Temple Place - Suite 330, Boston, MA  02111-1307, USA.
38  *
39  * Revisions   :
40  *    $Log: pcrs.c,v $
41  *    Revision 1.8  2001/06/29 21:45:41  oes
42  *    Indentation, CRLF->LF, Tab-> Space
43  *
44  *    Revision 1.7  2001/06/29 13:33:04  oes
45  *    - Cleaned up, renamed and reordered functions,
46  *      improved comments
47  *    - Removed my_strsep
48  *    - Replaced globalflag with a general flags int
49  *      that holds PCRS_GLOBAL, PCRS_SUCCESS, and PCRS_TRIVIAL
50  *    - Introduced trivial option that will prevent pcrs
51  *      from honouring backreferences in the substitute,
52  *      which is useful for large substitutes that are
53  *      red in from somewhere and saves the pain of escaping
54  *      the backrefs
55  *    - Introduced convenience function pcrs_free_joblist()
56  *    - Split pcrs_make_job() into pcrs_compile(), which still
57  *      takes a complete s/// comand as argument and parses it,
58  *      and a new function pcrs_make_job, which takes the
59  *      three separate components. This should make for a
60  *      much friendlier frontend.
61  *    - Removed create_pcrs_job() which was useless
62  *    - Fixed a bug in pcrs_execute
63  *    - Success flag is now handled by pcrs instead of user
64  *    - Removed logentry from cancelled commit
65  *
66  *    Revision 1.6  2001/06/03 19:12:45  oes
67  *    added FIXME
68  *
69  *    Revision 1.5  2001/05/29 09:50:24  jongfoster
70  *    Unified blocklist/imagelist/permissionslist.
71  *    File format is still under discussion, but the internal changes
72  *    are (mostly) done.
73  *
74  *    Also modified interceptor behaviour:
75  *    - We now intercept all URLs beginning with one of the following
76  *      prefixes (and *only* these prefixes):
77  *        * http://i.j.b/
78  *        * http://ijbswa.sf.net/config/
79  *        * http://ijbswa.sourceforge.net/config/
80  *    - New interceptors "home page" - go to http://i.j.b/ to see it.
81  *    - Internal changes so that intercepted and fast redirect pages
82  *      are not replaced with an image.
83  *    - Interceptors now have the option to send a binary page direct
84  *      to the client. (i.e. ijb-send-banner uses this)
85  *    - Implemented show-url-info interceptor.  (Which is why I needed
86  *      the above interceptors changes - a typical URL is
87  *      "http://i.j.b/show-url-info?url=www.somesite.com/banner.gif".
88  *      The previous mechanism would not have intercepted that, and
89  *      if it had been intercepted then it then it would have replaced
90  *      it with an image.)
91  *
92  *    Revision 1.4  2001/05/25 14:12:40  oes
93  *    Fixed bug: Empty substitutes now detected
94  *
95  *    Revision 1.3  2001/05/25 11:03:55  oes
96  *    Added sanity check for NULL jobs to pcrs_exec_substitution
97  *
98  *    Revision 1.2  2001/05/22 18:46:04  oes
99  *
100  *    - Enabled filtering banners by size rather than URL
101  *      by adding patterns that replace all standard banner
102  *      sizes with the "Junkbuster" gif to the re_filterfile
103  *
104  *    - Enabled filtering WebBugs by providing a pattern
105  *      which kills all 1x1 images
106  *
107  *    - Added support for PCRE_UNGREEDY behaviour to pcrs,
108  *      which is selected by the (nonstandard and therefore
109  *      capital) letter 'U' in the option string.
110  *      It causes the quantifiers to be ungreedy by default.
111  *      Appending a ? turns back to greedy (!).
112  *
113  *    - Added a new interceptor ijb-send-banner, which
114  *      sends back the "Junkbuster" gif. Without imagelist or
115  *      MSIE detection support, or if tinygif = 1, or the
116  *      URL isn't recognized as an imageurl, a lame HTML
117  *      explanation is sent instead.
118  *
119  *    - Added new feature, which permits blocking remote
120  *      script redirects and firing back a local redirect
121  *      to the browser.
122  *      The feature is conditionally compiled, i.e. it
123  *      can be disabled with --disable-fast-redirects,
124  *      plus it must be activated by a "fast-redirects"
125  *      line in the config file, has its own log level
126  *      and of course wants to be displayed by show-proxy-args
127  *      Note: Boy, all the #ifdefs in 1001 locations and
128  *      all the fumbling with configure.in and acconfig.h
129  *      were *way* more work than the feature itself :-(
130  *
131  *    - Because a generic redirect template was needed for
132  *      this, tinygif = 3 now uses the same.
133  *
134  *    - Moved GIFs, and other static HTTP response templates
135  *      to project.h
136  *
137  *    - Some minor fixes
138  *
139  *    - Removed some >400 CRs again (Jon, you really worked
140  *      a lot! ;-)
141  *
142  *    Revision 1.1.1.1  2001/05/15 13:59:02  oes
143  *    Initial import of version 2.9.3 source tree
144  *
145  *
146  *********************************************************************/
147 \f
148
149 #include <pcre.h>
150 #include <string.h>
151 #include "pcrs.h"
152 const char pcrs_h_rcs[] = PCRS_H_VERSION;
153
154
155 /*********************************************************************
156  *
157  * Function    :  pcrs_compile_perl_options
158  *
159  * Description :  This function parses a string containing the options to
160  *                Perl's s/// operator. It returns an integer that is the
161  *                pcre equivalent of the symbolic optstring.
162  *                Since pcre doesn't know about Perl's 'g' (global) or pcrs',
163  *                'T' (trivial) options but pcrs needs them, the corresponding
164  *                flags are set if 'g'or 'T' is encountered.
165  *                Note: The 'T' and 'U' options do not conform to Perl.
166  *             
167  * Parameters  :
168  *          1  :  optstring = string with options in perl syntax
169  *          2  :  flags = see description
170  *
171  * Returns     :  option integer suitable for pcre 
172  *
173  *********************************************************************/
174 int pcrs_compile_perl_options(char *optstring, int *flags)
175 {
176    size_t i;
177    int rc = 0;
178    *flags = 0;
179    for (i=0; i < strlen(optstring); i++)
180    {
181       switch(optstring[i])
182       {
183          case 'e': break;
184          case 'g': *flags |= PCRS_GLOBAL; break;
185          case 'i': rc |= PCRE_CASELESS; break;
186          case 'm': rc |= PCRE_MULTILINE; break;
187          case 'o': break;
188          case 's': rc |= PCRE_DOTALL; break;
189          case 'x': rc |= PCRE_EXTENDED; break;
190          case 'U': rc |= PCRE_UNGREEDY; break;
191            case 'T': *flags |= PCRS_TRIVIAL; break;
192          default:  break;
193       }
194    }
195    return rc;
196
197 }
198
199
200 /*********************************************************************
201  *
202  * Function    :  pcrs_compile_replacement
203  *
204  * Description :  This function takes a Perl-style replacement (2nd argument
205  *                to the s/// operator and returns a compiled pcrs_substitute,
206  *                or NULL if memory allocation for the substitute structure
207  *                fails.
208  *
209  * Parameters  :
210  *          1  :  replacement = replacement part of s/// operator
211  *                              in perl syntax
212  *          2  :  errptr = pointer to an integer in which error
213  *                         conditions can be returned.
214  *
215  * Returns     :  pcrs_substitute data structure, or NULL if an
216  *                error is encountered. In that case, *errptr has
217  *                the reason.
218  *
219  *********************************************************************/
220 pcrs_substitute *pcrs_compile_replacement(char *replacement, int trivialflag, int *errptr)
221 {
222    int length, i, k = 0, l = 0, quoted = 0, idx;
223    char *text, *num_ptr, *numbers = "0123456789";
224    pcrs_substitute *r;
225
226    r = (pcrs_substitute *)malloc(sizeof(pcrs_substitute));
227    if (r == NULL) return NULL;
228    memset(r, '\0', sizeof(pcrs_substitute));
229
230    text = strdup(replacement);      /* must be free()d by caller */
231    if (text  == NULL)
232    {
233       *errptr = PCRS_ERR_NOMEM;
234       free(r);
235       return NULL;
236    }
237
238    length = strlen(replacement);
239
240    if (trivialflag)
241    {
242        k = length;
243    }
244    else
245    {
246       for (i=0; i < length; i++)
247       {
248          /* Backslash treatment */
249          if (replacement[i] == '\\')
250          {
251             if (quoted)
252             {
253                text[k++] = replacement[i];
254                quoted = 0;
255             }
256             else
257             {
258                quoted = 1;
259             }
260             continue;
261          }
262
263          /* Dollar treatment */
264          if (replacement[i] == '$' && !quoted && i < length - 1)
265          {
266             if (strchr("0123456789&", replacement[i + 1]) == NULL)
267             {
268                text[k++] = replacement[i];
269             }
270             else
271             {
272                r->block_length[l] = k - r->block_offset[l];
273                r->backref[l] = 0;
274                if (replacement[i + 1] != '&')
275                {
276                   while ((num_ptr = strchr(numbers, replacement[++i])) != NULL && i < length)
277                   {
278                      idx = num_ptr - numbers;
279                      r->backref[l] = r->backref[l] * 10 + idx;
280                   }
281                   i--;
282                }
283                else
284                   i++;
285                if (r->backref[l] < PCRS_MAX_SUBMATCHES)
286                   r->backref_count[r->backref[l]] += 1;
287                l++;
288                r->block_offset[l] = k;
289             }
290             continue;
291          }
292
293          /* Plain char treatment */
294          text[k++] = replacement[i];
295          quoted = 0;
296       }
297    } /* -END- if (!trivialflag) */
298
299    text[k] = '\0';
300    r->text = text;
301    r->backrefs = l;
302    r->block_length[l] = k - r->block_offset[l];
303    return r;
304
305 }
306
307
308 /*********************************************************************
309  *
310  * Function    :  pcrs_free_job
311  *
312  * Description :  Frees the memory used by a pcrs_job struct and its
313  *                dependant structures. Returns a pointer to the next
314  *                job, if there was any, or NULL otherwise.
315  *
316  * Parameters  :
317  *          1  :  job = pointer to the pcrs_job structure to be freed
318  *
319  * Returns     :  a pointer to the next job, if there was any, or
320  *                NULL otherwise. 
321  *
322  *********************************************************************/
323 pcrs_job *pcrs_free_job(pcrs_job *job)
324 {
325    pcrs_job *next;
326
327    if (job == NULL)
328    {
329       return NULL;
330    }
331    else
332    {
333       next = job->next;
334       if (job->pattern != NULL) free(job->pattern);
335       if (job->hints != NULL) free(job->hints);
336       if (job->substitute != NULL)
337       {
338          if (job->substitute->text != NULL) free(job->substitute->text);
339          free(job->substitute);
340       }
341       free(job);
342    }
343    return next;
344
345 }
346
347 /*********************************************************************
348  *
349  * Function    :  pcrs_free_joblist
350  *
351  * Description :  Iterates through a chained list of pcrs_job's and
352  *                frees them using pcrs_free_job.
353  *
354  * Parameters  :
355  *          1  :  joblist = pointer to the first pcrs_job structure to
356  *                be freed
357  *
358  * Returns     :  N/A
359  *
360  *********************************************************************/
361 void pcrs_free_joblist(pcrs_job *joblist)
362 {
363    while ( NULL != (joblist = pcrs_free_job(joblist)) ) {};
364
365    return;
366
367 }
368
369
370 /*********************************************************************
371  *
372  * Function    :  pcrs_compile_command
373  *
374  * Description :  Parses a string with a Perl-style s/// command, 
375  *                calls pcrs_compile, and returns a corresponding
376  *                pcrs_job, or NULL if parsing or compiling the job
377  *                fails.
378  *
379  * Parameters  :
380  *          1  :  command = string with perl-style s/// command
381  *          2  :  errptr = pointer to an integer in which error
382  *                         conditions can be returned.
383  *
384  * Returns     :  a corresponding pcrs_job data structure, or NULL
385  *                if an error was encountered. In that case, *errptr
386  *                has the reason.
387  *
388  *********************************************************************/
389 pcrs_job *pcrs_compile_command(char *command, int *errptr)
390 {
391    int i, k, l, limit, quoted = FALSE;
392    char delimiter;
393    char *tokens[4];   
394    pcrs_job *newjob;
395
396    i = k = l = 0;
397
398    /*
399     * Tokenize the perl command
400     */
401    limit = strlen(command);
402    if (limit < 4)
403    {
404       *errptr = PCRS_ERR_CMDSYNTAX;
405       return NULL;
406    }
407    else
408    {
409      delimiter = command[1];
410    }
411
412    tokens[l] = (char *) malloc(limit + 1);
413
414    for (i=0; i <= limit; i++)
415    {
416
417       if (command[i] == delimiter && !quoted)
418       {
419            if (l == 3)
420                 {
421                    l = -1;
422             break;
423          }
424           tokens[0][k++] = '\0';
425          tokens[++l] = tokens[0] + k;
426          continue;
427       }
428
429       else if (command[i] == '\\' && !quoted && i+1 < limit && command[i+1] == delimiter)
430       {
431          quoted = TRUE;
432          continue;
433       }
434       tokens[0][k++] = command[i];
435       quoted = FALSE;
436    }
437
438
439    /*
440     * Syntax error ?
441     */
442    if (l != 3)
443    {
444       *errptr = PCRS_ERR_CMDSYNTAX;
445       free(tokens[0]);
446       return NULL;
447    }
448
449    newjob = pcrs_compile(tokens[1], tokens[2], tokens[3], errptr);
450    free(tokens[0]);
451    return newjob;
452
453 }
454
455
456 /*********************************************************************
457  *
458  * Function    :  pcrs_compile
459  *
460  * Description :  Takes the three arguments to a perl s/// command
461  *                and compiles a pcrs_job structure from them.
462  *
463  * Parameters  :
464  *          1  :  pattern = string with perl-style pattern
465  *          2  :  substitute = string with perl-style substitute
466  *          3  :  options = string with perl-style options
467  *          4  :  errptr = pointer to an integer in which error
468  *                         conditions can be returned.
469  *
470  * Returns     :  a corresponding pcrs_job data structure, or NULL
471  *                if an error was encountered. In that case, *errptr
472  *                has the reason.
473  *
474  *********************************************************************/
475 pcrs_job *pcrs_compile(char *pattern, char *substitute, char *options, int *errptr)
476 {
477    pcrs_job *newjob;
478    int flags;
479    const char *error;
480
481
482    /* 
483     * Handle NULL arguments
484     */
485    if (pattern == NULL) pattern = "";
486    if (substitute == NULL) substitute = "";
487    if (options == NULL) options = "";
488
489
490    /* 
491     * Get and init memory
492     */
493    if (NULL == (newjob = (pcrs_job *)malloc(sizeof(pcrs_job))))
494    {
495       *errptr = PCRS_ERR_NOMEM;
496       return NULL;
497    }
498    memset(newjob, '\0', sizeof(pcrs_job));
499
500
501    /*
502     * Evaluate the options
503     */
504    newjob->options = pcrs_compile_perl_options(options, &flags);
505    newjob->flags = flags;
506
507
508    /*
509     * Compile the pattern
510     */
511    newjob->pattern = pcre_compile(pattern, newjob->options, &error, errptr, NULL);
512    if (newjob->pattern == NULL)
513    {
514       pcrs_free_job(newjob);
515       return NULL;
516    }
517
518
519    /*
520     * Generate hints. This has little overhead, since the
521     * hints will be NULL for a boring pattern anyway.
522     */
523    newjob->hints = pcre_study(newjob->pattern, 0, &error);
524    if (error != NULL)
525    {
526       *errptr = PCRS_ERR_STUDY;
527       pcrs_free_job(newjob);
528       return NULL;
529    }
530  
531
532    /*
533     * Compile the substitute
534     */
535    if (NULL == (newjob->substitute = pcrs_compile_replacement(substitute, newjob->flags & PCRS_TRIVIAL, errptr)))
536    {
537       pcrs_free_job(newjob);
538       return NULL;
539    }
540  
541    return newjob;
542
543 }
544
545
546 /*********************************************************************
547  *
548  * Function    :  pcrs_execute
549  *
550  * Description :  Modify the subject by executing the regular substitution
551  *                defined by the job. Since the result may be longer than
552  *                the subject, its space requirements are precalculated in
553  *                the matching phase and new memory is allocated accordingly.
554  *                It is the caller's responsibility to free the result when
555  *                it's no longer needed.
556  *
557  * Parameters  :
558  *          1  :  job = the pcrs_job to be executed
559  *          2  :  subject = the subject (== original) string
560  *          3  :  subject_length = the subject's length 
561  *                INCLUDING the terminating zero, if string!
562  *          4  :  result = char** for returning  the result 
563  *          5  :  result_length = int* for returning the result's length
564  *
565  * Returns     :  the number of substitutions that were made. May be > 1
566  *                if job->flags contained PCRS_GLOBAL
567  *
568  *********************************************************************/
569 int pcrs_execute(pcrs_job *job, char *subject, int subject_length, char **result, int *result_length)
570 {
571    int offsets[3 * PCRS_MAX_SUBMATCHES],
572        offset, i, k,
573        matches_found,
574        newsize,
575        submatches;
576    pcrs_match matches[PCRS_MAX_MATCHES];
577    char *result_offset;
578
579    offset = i = k = 0;
580
581    /* 
582     * Sanity check
583     */
584    if (job == NULL || job->pattern == NULL || job->substitute == NULL)
585    {
586       *result = NULL;
587       return(PCRS_ERR_BADJOB);
588    }
589
590    
591    /*
592     * Find the pattern and calculate the space
593     * requirements for the result (newsize)
594     */
595    newsize=subject_length;
596
597    while ((submatches = pcre_exec(job->pattern, job->hints, subject, subject_length, offset, 0, offsets, 3 * PCRS_MAX_SUBMATCHES)) > 0)
598    {
599       job->flags |= PCRS_SUCCESS;
600       matches[i].submatches = submatches;
601       for (k=0; k < submatches; k++)
602       {
603          matches[i].submatch_offset[k] = offsets[2 * k];
604
605          /* Note: Non-found optional submatches have length -1-(-1)==0 */
606          matches[i].submatch_length[k] = offsets[2 * k + 1] - offsets[2 * k]; 
607
608          /* reserve mem for each submatch as often as it is ref'd */
609          newsize += matches[i].submatch_length[k] * job->substitute->backref_count[k]; 
610       }
611       /* plus replacement text size minus match text size */
612       newsize += strlen(job->substitute->text) - matches[i].submatch_length[0]; 
613
614       /* Non-global search or limit reached? */
615       if (++i >= PCRS_MAX_MATCHES || !(job->flags & PCRS_GLOBAL) ) break;
616
617       /* Don't loop on empty matches */
618       if (offsets[1] == offset)
619          if (offset < subject_length)
620             offset++;
621          else
622             break;
623       /* Go find the next one */
624       else
625          offset = offsets[1];
626    }
627    /* Pass pcre error through if failiure */
628    if (submatches < -1) return submatches;   
629    matches_found = i;
630
631
632    /* 
633     * Get memory for the result
634     */
635    if ((*result = (char *)malloc(newsize)) == NULL)   /* must be free()d by caller */
636    {
637       return PCRS_ERR_NOMEM;
638    }
639
640
641    /* 
642     * Replace
643     */
644    offset = 0;
645    result_offset = *result;
646
647    for (i=0; i < matches_found; i++)
648    {
649       /* copy the chunk preceding the match */
650       memcpy(result_offset, subject + offset, matches[i].submatch_offset[0] - offset); 
651       result_offset += matches[i].submatch_offset[0] - offset;
652
653       /* For every segment of the substitute.. */
654       for (k=0; k <= job->substitute->backrefs; k++)
655       {
656          /* ...copy its text.. */
657          memcpy(result_offset, job->substitute->text + job->substitute->block_offset[k], job->substitute->block_length[k]);
658          result_offset += job->substitute->block_length[k];
659
660          /* ..plus, if it's not the last chunk, i.e.: There *is* a backref.. */
661          if (k != job->substitute->backrefs
662              /* ..in legal range.. */
663              && job->substitute->backref[k] <= PCRS_MAX_SUBMATCHES
664              /* ..and referencing a nonempty match.. */
665              && matches[i].submatch_length[job->substitute->backref[k]] > 0)
666          {
667             /* ..copy the submatch that is ref'd. */
668             memcpy(
669                result_offset,
670                subject + matches[i].submatch_offset[job->substitute->backref[k]],
671                matches[i].submatch_length[job->substitute->backref[k]]
672             );
673             result_offset += matches[i].submatch_length[job->substitute->backref[k]];
674          }
675       }
676       offset =  matches[i].submatch_offset[0] + matches[i].submatch_length[0];
677    }
678
679    /* Copy the rest. */
680    memcpy(result_offset, subject + offset, subject_length - offset);
681
682    *result_length = newsize;
683    return matches_found;
684
685 }
686
687
688 /*
689   Local Variables:
690   tab-width: 3
691   end:
692 */