|
1 /* |
|
2 * Copyright (c) 2005, 2008 Sun Microsystems, Inc. All Rights Reserved. |
|
3 * Use is subject to license terms. |
|
4 * |
|
5 * Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 * you may not use this file except in compliance with the License. |
|
7 * You may obtain a copy of the License at |
|
8 * http://www.apache.org/licenses/LICENSE-2.0. |
|
9 * |
|
10 * Unless required by applicable law or agreed to in writing, software |
|
11 * distributed under the License is distributed on an "AS IS" BASIS, |
|
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
|
13 * or implied. |
|
14 * See the License for the specific language governing permissions and |
|
15 * limitations under the License. |
|
16 */ |
|
17 |
|
18 #include "httpd.h" |
|
19 #include "http_config.h" |
|
20 #include "http_log.h" |
|
21 #include "apr_strings.h" |
|
22 #include "apr_general.h" |
|
23 #include "util_filter.h" |
|
24 #include "apr_buckets.h" |
|
25 #include "http_request.h" |
|
26 #include "libsed.h" |
|
27 |
|
28 static const char *sed_filter_name = "Sed"; |
|
29 #define MODSED_OUTBUF_SIZE 8000 |
|
30 #define MAX_TRANSIENT_BUCKETS 50 |
|
31 |
|
32 typedef struct sed_expr_config |
|
33 { |
|
34 sed_commands_t *sed_cmds; |
|
35 const char *last_error; |
|
36 } sed_expr_config; |
|
37 |
|
38 typedef struct sed_config |
|
39 { |
|
40 sed_expr_config output; |
|
41 sed_expr_config input; |
|
42 } sed_config; |
|
43 |
|
44 /* Context for filter invocation for single HTTP request */ |
|
45 typedef struct sed_filter_ctxt |
|
46 { |
|
47 sed_eval_t eval; |
|
48 ap_filter_t *f; |
|
49 request_rec *r; |
|
50 apr_bucket_brigade *bb; |
|
51 char *outbuf; |
|
52 char *curoutbuf; |
|
53 int bufsize; |
|
54 apr_pool_t *tpool; |
|
55 int numbuckets; |
|
56 } sed_filter_ctxt; |
|
57 |
|
58 module AP_MODULE_DECLARE_DATA sed_module; |
|
59 |
|
60 /* This function will be call back from libsed functions if there is any error |
|
61 * happend during execution of sed scripts |
|
62 */ |
|
63 static apr_status_t log_sed_errf(void *data, const char *error) |
|
64 { |
|
65 request_rec *r = (request_rec *) data; |
|
66 ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, error); |
|
67 return APR_SUCCESS; |
|
68 } |
|
69 |
|
70 /* This function will be call back from libsed functions if there is any |
|
71 * compilation error. |
|
72 */ |
|
73 static apr_status_t sed_compile_errf(void *data, const char *error) |
|
74 { |
|
75 sed_expr_config *sed_cfg = (sed_expr_config *) data; |
|
76 sed_cfg->last_error = error; |
|
77 return APR_SUCCESS; |
|
78 } |
|
79 |
|
80 /* clear the temporary pool (used for transient buckets) |
|
81 */ |
|
82 static void clear_ctxpool(sed_filter_ctxt* ctx) |
|
83 { |
|
84 apr_pool_clear(ctx->tpool); |
|
85 ctx->outbuf = NULL; |
|
86 ctx->curoutbuf = NULL; |
|
87 ctx->numbuckets = 0; |
|
88 } |
|
89 |
|
90 /* alloc_outbuf |
|
91 * allocate output buffer |
|
92 */ |
|
93 static void alloc_outbuf(sed_filter_ctxt* ctx) |
|
94 { |
|
95 ctx->outbuf = apr_palloc(ctx->tpool, ctx->bufsize + 1); |
|
96 ctx->curoutbuf = ctx->outbuf; |
|
97 } |
|
98 |
|
99 /* append_bucket |
|
100 * Allocate a new bucket from buf and sz and append to ctx->bb |
|
101 */ |
|
102 static apr_status_t append_bucket(sed_filter_ctxt* ctx, char* buf, int sz) |
|
103 { |
|
104 apr_status_t status = APR_SUCCESS; |
|
105 apr_bucket *b; |
|
106 if (ctx->tpool == ctx->r->pool) { |
|
107 /* We are not using transient bucket */ |
|
108 b = apr_bucket_pool_create(buf, sz, ctx->r->pool, |
|
109 ctx->r->connection->bucket_alloc); |
|
110 APR_BRIGADE_INSERT_TAIL(ctx->bb, b); |
|
111 } |
|
112 else { |
|
113 /* We are using transient bucket */ |
|
114 b = apr_bucket_transient_create(buf, sz, |
|
115 ctx->r->connection->bucket_alloc); |
|
116 APR_BRIGADE_INSERT_TAIL(ctx->bb, b); |
|
117 ctx->numbuckets++; |
|
118 if (ctx->numbuckets >= MAX_TRANSIENT_BUCKETS) { |
|
119 b = apr_bucket_flush_create(ctx->r->connection->bucket_alloc); |
|
120 APR_BRIGADE_INSERT_TAIL(ctx->bb, b); |
|
121 status = ap_pass_brigade(ctx->f->next, ctx->bb); |
|
122 apr_brigade_cleanup(ctx->bb); |
|
123 clear_ctxpool(ctx); |
|
124 } |
|
125 } |
|
126 return status; |
|
127 } |
|
128 |
|
129 /* |
|
130 * flush_output_buffer |
|
131 * Flush the output data (stored in ctx->outbuf) |
|
132 */ |
|
133 static apr_status_t flush_output_buffer(sed_filter_ctxt *ctx) |
|
134 { |
|
135 int size = ctx->curoutbuf - ctx->outbuf; |
|
136 char *out; |
|
137 apr_status_t status = APR_SUCCESS; |
|
138 if ((ctx->outbuf == NULL) || (size <=0)) |
|
139 return status; |
|
140 out = apr_palloc(ctx->tpool, size); |
|
141 memcpy(out, ctx->outbuf, size); |
|
142 status = append_bucket(ctx, out, size); |
|
143 ctx->curoutbuf = ctx->outbuf; |
|
144 return status; |
|
145 } |
|
146 |
|
147 /* This is a call back function. When libsed wants to generate the output, |
|
148 * this function will be invoked. |
|
149 */ |
|
150 static apr_status_t sed_write_output(void *dummy, char *buf, int sz) |
|
151 { |
|
152 /* dummy is basically filter context. Context is passed during invocation |
|
153 * of sed_eval_buffer |
|
154 */ |
|
155 int remainbytes = 0; |
|
156 apr_status_t status = APR_SUCCESS; |
|
157 sed_filter_ctxt *ctx = (sed_filter_ctxt *) dummy; |
|
158 if (ctx->outbuf == NULL) { |
|
159 alloc_outbuf(ctx); |
|
160 } |
|
161 remainbytes = ctx->bufsize - (ctx->curoutbuf - ctx->outbuf); |
|
162 if (sz >= remainbytes) { |
|
163 if (remainbytes > 0) { |
|
164 memcpy(ctx->curoutbuf, buf, remainbytes); |
|
165 buf += remainbytes; |
|
166 sz -= remainbytes; |
|
167 ctx->curoutbuf += remainbytes; |
|
168 } |
|
169 /* buffer is now full */ |
|
170 status = append_bucket(ctx, ctx->outbuf, ctx->bufsize); |
|
171 /* old buffer is now used so allocate new buffer */ |
|
172 alloc_outbuf(ctx); |
|
173 /* if size is bigger than the allocated buffer directly add to output |
|
174 * brigade */ |
|
175 if ((status == APR_SUCCESS) && (sz >= ctx->bufsize)) { |
|
176 char* newbuf = apr_palloc(ctx->tpool, sz); |
|
177 memcpy(newbuf, buf, sz); |
|
178 status = append_bucket(ctx, newbuf, sz); |
|
179 /* pool might get clear after append_bucket */ |
|
180 if (ctx->outbuf == NULL) { |
|
181 alloc_outbuf(ctx); |
|
182 } |
|
183 } |
|
184 else { |
|
185 memcpy(ctx->curoutbuf, buf, sz); |
|
186 ctx->curoutbuf += sz; |
|
187 } |
|
188 } |
|
189 else { |
|
190 memcpy(ctx->curoutbuf, buf, sz); |
|
191 ctx->curoutbuf += sz; |
|
192 } |
|
193 return status; |
|
194 } |
|
195 |
|
196 /* Compile a sed expression. Compiled context is saved in sed_cfg->sed_cmds. |
|
197 * Memory required for compilation context is allocated from cmd->pool. |
|
198 */ |
|
199 static apr_status_t compile_sed_expr(sed_expr_config *sed_cfg, |
|
200 cmd_parms *cmd, |
|
201 const char *expr) |
|
202 { |
|
203 apr_status_t status = APR_SUCCESS; |
|
204 |
|
205 if (!sed_cfg->sed_cmds) { |
|
206 sed_commands_t *sed_cmds; |
|
207 sed_cmds = apr_pcalloc(cmd->pool, sizeof(sed_commands_t)); |
|
208 status = sed_init_commands(sed_cmds, sed_compile_errf, sed_cfg, |
|
209 cmd->pool); |
|
210 if (status != APR_SUCCESS) { |
|
211 sed_destroy_commands(sed_cmds); |
|
212 return status; |
|
213 } |
|
214 sed_cfg->sed_cmds = sed_cmds; |
|
215 } |
|
216 status = sed_compile_string(sed_cfg->sed_cmds, expr); |
|
217 if (status != APR_SUCCESS) { |
|
218 sed_destroy_commands(sed_cfg->sed_cmds); |
|
219 sed_cfg->sed_cmds = NULL; |
|
220 } |
|
221 return status; |
|
222 } |
|
223 |
|
224 /* sed eval cleanup function */ |
|
225 static apr_status_t sed_eval_cleanup(void *data) |
|
226 { |
|
227 sed_eval_t *eval = (sed_eval_t *) data; |
|
228 sed_destroy_eval(eval); |
|
229 return APR_SUCCESS; |
|
230 } |
|
231 |
|
232 /* Initialize sed filter context. If successful then context is set in f->ctx |
|
233 */ |
|
234 static apr_status_t init_context(ap_filter_t *f, sed_expr_config *sed_cfg, int usetpool) |
|
235 { |
|
236 apr_status_t status; |
|
237 sed_filter_ctxt* ctx; |
|
238 request_rec *r = f->r; |
|
239 /* Create the context. Call sed_init_eval. libsed will generated |
|
240 * output by calling sed_write_output and generates any error by |
|
241 * invoking log_sed_errf. |
|
242 */ |
|
243 ctx = apr_pcalloc(r->pool, sizeof(sed_filter_ctxt)); |
|
244 ctx->r = r; |
|
245 ctx->bb = NULL; |
|
246 ctx->numbuckets = 0; |
|
247 ctx->f = f; |
|
248 status = sed_init_eval(&ctx->eval, sed_cfg->sed_cmds, log_sed_errf, |
|
249 r, &sed_write_output, r->pool); |
|
250 if (status != APR_SUCCESS) { |
|
251 return status; |
|
252 } |
|
253 apr_pool_cleanup_register(r->pool, &ctx->eval, sed_eval_cleanup, |
|
254 apr_pool_cleanup_null); |
|
255 ctx->bufsize = MODSED_OUTBUF_SIZE; |
|
256 if (usetpool) { |
|
257 apr_pool_create(&(ctx->tpool), r->pool); |
|
258 } |
|
259 else { |
|
260 ctx->tpool = r->pool; |
|
261 } |
|
262 alloc_outbuf(ctx); |
|
263 f->ctx = ctx; |
|
264 return APR_SUCCESS; |
|
265 } |
|
266 |
|
267 /* Entry function for Sed output filter */ |
|
268 static apr_status_t sed_response_filter(ap_filter_t *f, |
|
269 apr_bucket_brigade *bb) |
|
270 { |
|
271 apr_bucket *b; |
|
272 apr_status_t status; |
|
273 sed_config *cfg = ap_get_module_config(f->r->per_dir_config, |
|
274 &sed_module); |
|
275 sed_filter_ctxt *ctx = f->ctx; |
|
276 sed_expr_config *sed_cfg = &cfg->output; |
|
277 |
|
278 if ((sed_cfg == NULL) || (sed_cfg->sed_cmds == NULL)) { |
|
279 /* No sed expressions */ |
|
280 ap_remove_output_filter(f); |
|
281 return ap_pass_brigade(f->next, bb); |
|
282 } |
|
283 |
|
284 if (ctx == NULL) { |
|
285 |
|
286 if (APR_BUCKET_IS_EOS(APR_BRIGADE_FIRST(bb))) { |
|
287 /* no need to run sed filter for Head requests */ |
|
288 ap_remove_output_filter(f); |
|
289 return ap_pass_brigade(f->next, bb); |
|
290 } |
|
291 |
|
292 status = init_context(f, sed_cfg, 1); |
|
293 if (status != APR_SUCCESS) |
|
294 return status; |
|
295 ctx = f->ctx; |
|
296 apr_table_unset(f->r->headers_out, "Content-Length"); |
|
297 } |
|
298 |
|
299 ctx->bb = apr_brigade_create(f->r->pool, f->c->bucket_alloc); |
|
300 |
|
301 /* Here is the main logic. Iterate through all the buckets, read the |
|
302 * content of the bucket, call sed_eval_buffer on the data. |
|
303 * sed_eval_buffer will read the data line by line, run filters on each |
|
304 * line. sed_eval_buffer will generates the output by calling |
|
305 * sed_write_output which will add the output to ctx->bb. At the end of |
|
306 * the loop, ctx->bb is passed to the next filter in chain. At the end of |
|
307 * the data, if new line is not found then sed_eval_buffer will store the |
|
308 * data in it's own buffer. |
|
309 * |
|
310 * Once eos bucket is found then sed_finalize_eval will flush the rest of |
|
311 * the data. If there is no new line in last line of data, new line is |
|
312 * appended (that is a solaris sed behavior). libsed's internal memory for |
|
313 * evaluation is allocated on request's pool so it will be cleared once |
|
314 * request is over. |
|
315 * |
|
316 * If flush bucket is found then append the the flush bucket to ctx->bb |
|
317 * and pass it to next filter. There may be some data which will still be |
|
318 * in sed's internal buffer which can't be flushed until new line |
|
319 * character is arrived. |
|
320 */ |
|
321 for (b = APR_BRIGADE_FIRST(bb); b != APR_BRIGADE_SENTINEL(bb);) { |
|
322 const char *buf = NULL; |
|
323 apr_size_t bytes = 0; |
|
324 if (APR_BUCKET_IS_EOS(b)) { |
|
325 apr_bucket *b1 = APR_BUCKET_NEXT(b); |
|
326 /* Now clean up the internal sed buffer */ |
|
327 sed_finalize_eval(&ctx->eval, ctx); |
|
328 status = flush_output_buffer(ctx); |
|
329 if (status != APR_SUCCESS) { |
|
330 clear_ctxpool(ctx); |
|
331 return status; |
|
332 } |
|
333 APR_BUCKET_REMOVE(b); |
|
334 /* Insert the eos bucket to ctx->bb brigade */ |
|
335 APR_BRIGADE_INSERT_TAIL(ctx->bb, b); |
|
336 b = b1; |
|
337 } |
|
338 else if (APR_BUCKET_IS_FLUSH(b)) { |
|
339 apr_bucket *b1 = APR_BUCKET_NEXT(b); |
|
340 APR_BUCKET_REMOVE(b); |
|
341 status = flush_output_buffer(ctx); |
|
342 if (status != APR_SUCCESS) { |
|
343 clear_ctxpool(ctx); |
|
344 return status; |
|
345 } |
|
346 APR_BRIGADE_INSERT_TAIL(ctx->bb, b); |
|
347 b = b1; |
|
348 } |
|
349 else if (APR_BUCKET_IS_METADATA(b)) { |
|
350 b = APR_BUCKET_NEXT(b); |
|
351 } |
|
352 else if (apr_bucket_read(b, &buf, &bytes, APR_BLOCK_READ) |
|
353 == APR_SUCCESS) { |
|
354 apr_bucket *b1 = APR_BUCKET_NEXT(b); |
|
355 status = sed_eval_buffer(&ctx->eval, buf, bytes, ctx); |
|
356 if (status != APR_SUCCESS) { |
|
357 clear_ctxpool(ctx); |
|
358 return status; |
|
359 } |
|
360 APR_BUCKET_REMOVE(b); |
|
361 apr_bucket_delete(b); |
|
362 b = b1; |
|
363 } |
|
364 else { |
|
365 apr_bucket *b1 = APR_BUCKET_NEXT(b); |
|
366 APR_BUCKET_REMOVE(b); |
|
367 b = b1; |
|
368 } |
|
369 } |
|
370 apr_brigade_cleanup(bb); |
|
371 status = flush_output_buffer(ctx); |
|
372 if (status != APR_SUCCESS) { |
|
373 clear_ctxpool(ctx); |
|
374 return status; |
|
375 } |
|
376 if (!APR_BRIGADE_EMPTY(ctx->bb)) { |
|
377 status = ap_pass_brigade(f->next, ctx->bb); |
|
378 apr_brigade_cleanup(ctx->bb); |
|
379 } |
|
380 clear_ctxpool(ctx); |
|
381 return status; |
|
382 } |
|
383 |
|
384 /* Entry function for Sed input filter */ |
|
385 static apr_status_t sed_request_filter(ap_filter_t *f, |
|
386 apr_bucket_brigade *bb, |
|
387 ap_input_mode_t mode, |
|
388 apr_read_type_e block, |
|
389 apr_off_t readbytes) |
|
390 { |
|
391 sed_config *cfg = ap_get_module_config(f->r->per_dir_config, |
|
392 &sed_module); |
|
393 sed_filter_ctxt *ctx = f->ctx; |
|
394 apr_status_t status; |
|
395 sed_expr_config *sed_cfg = &cfg->input; |
|
396 |
|
397 if (mode != AP_MODE_READBYTES) { |
|
398 return ap_get_brigade(f->next, bb, mode, block, readbytes); |
|
399 } |
|
400 |
|
401 if ((sed_cfg == NULL) || (sed_cfg->sed_cmds == NULL)) { |
|
402 /* No sed expression */ |
|
403 return ap_get_brigade(f->next, bb, mode, block, readbytes); |
|
404 } |
|
405 |
|
406 if (!ctx) { |
|
407 if (!ap_is_initial_req(f->r)) { |
|
408 ap_remove_input_filter(f); |
|
409 /* XXX : Should we filter the sub requests too */ |
|
410 return ap_get_brigade(f->next, bb, mode, block, readbytes); |
|
411 } |
|
412 status = init_context(f, sed_cfg, 0); |
|
413 if (status != APR_SUCCESS) |
|
414 return status; |
|
415 ctx = f->ctx; |
|
416 ctx->bb = apr_brigade_create(f->r->pool, f->c->bucket_alloc); |
|
417 } |
|
418 |
|
419 /* Here is the logic : |
|
420 * Read the readbytes data from next level fiter into bbinp. Loop through |
|
421 * the buckets in bbinp and read the data from buckets and invoke |
|
422 * sed_eval_buffer on the data. libsed will generate it's output using |
|
423 * sed_write_output which will add data in ctx->bb. Do it until it have |
|
424 * atleast one bucket bucket in ctx->bb. At the end of data eos bucket |
|
425 * should be there. |
|
426 * |
|
427 * Once eos bucket is seen, then invoke sed_finalize_eval to clear the |
|
428 * output. If the last byte of data is not a new line character then sed |
|
429 * will add a new line to the data that is default sed behaviour. Note |
|
430 * that using this filter with POST data, caller may not expect this |
|
431 * behaviour. |
|
432 * |
|
433 * If next level fiter generate the flush bucket, we can't do much about |
|
434 * it. If we want to return the flush bucket in brigade bb (to the caller) |
|
435 * the question is where to add it? |
|
436 */ |
|
437 while (APR_BRIGADE_EMPTY(ctx->bb)) { |
|
438 apr_bucket_brigade *bbinp; |
|
439 apr_bucket *b; |
|
440 |
|
441 /* read the bytes from next level filter */ |
|
442 bbinp = apr_brigade_create(f->r->pool, f->c->bucket_alloc); |
|
443 status = ap_get_brigade(f->next, bbinp, mode, block, readbytes); |
|
444 if (status != APR_SUCCESS) { |
|
445 return status; |
|
446 } |
|
447 for (b = APR_BRIGADE_FIRST(bbinp); b != APR_BRIGADE_SENTINEL(bbinp); |
|
448 b = APR_BUCKET_NEXT(b)) { |
|
449 const char *buf = NULL; |
|
450 apr_size_t bytes; |
|
451 |
|
452 if (APR_BUCKET_IS_EOS(b)) { |
|
453 /* eos bucket. Clear the internal sed buffers */ |
|
454 sed_finalize_eval(&ctx->eval, ctx); |
|
455 flush_output_buffer(ctx); |
|
456 APR_BUCKET_REMOVE(b); |
|
457 APR_BRIGADE_INSERT_TAIL(ctx->bb, b); |
|
458 break; |
|
459 } |
|
460 else if (APR_BUCKET_IS_FLUSH(b)) { |
|
461 /* What should we do with flush bucket */ |
|
462 continue; |
|
463 } |
|
464 if (apr_bucket_read(b, &buf, &bytes, APR_BLOCK_READ) |
|
465 == APR_SUCCESS) { |
|
466 status = sed_eval_buffer(&ctx->eval, buf, bytes, ctx); |
|
467 if (status != APR_SUCCESS) |
|
468 return status; |
|
469 flush_output_buffer(ctx); |
|
470 } |
|
471 } |
|
472 apr_brigade_cleanup(bbinp); |
|
473 apr_brigade_destroy(bbinp); |
|
474 } |
|
475 |
|
476 if (!APR_BRIGADE_EMPTY(ctx->bb)) { |
|
477 apr_bucket_brigade *newbb = NULL; |
|
478 apr_bucket *b = NULL; |
|
479 |
|
480 /* This may return APR_INCOMPLETE which should be fine */ |
|
481 apr_brigade_partition(ctx->bb, readbytes, &b); |
|
482 |
|
483 newbb = apr_brigade_split(ctx->bb, b); |
|
484 APR_BRIGADE_CONCAT(bb, ctx->bb); |
|
485 APR_BRIGADE_CONCAT(ctx->bb, newbb); |
|
486 } |
|
487 return APR_SUCCESS; |
|
488 } |
|
489 |
|
490 static const char *sed_add_expr(cmd_parms *cmd, void *cfg, const char *arg) |
|
491 { |
|
492 int offset = (int) (long) cmd->info; |
|
493 sed_expr_config *sed_cfg = |
|
494 (sed_expr_config *) (((char *) cfg) + offset); |
|
495 if (compile_sed_expr(sed_cfg, cmd, arg) != APR_SUCCESS) { |
|
496 return apr_psprintf(cmd->temp_pool, |
|
497 "Failed to compile sed expression. %s", |
|
498 sed_cfg->last_error); |
|
499 } |
|
500 return NULL; |
|
501 } |
|
502 |
|
503 static void *create_sed_dir_config(apr_pool_t *p, char *s) |
|
504 { |
|
505 sed_config *cfg = apr_pcalloc(p, sizeof(sed_config)); |
|
506 return cfg; |
|
507 } |
|
508 |
|
509 static const command_rec sed_filter_cmds[] = { |
|
510 AP_INIT_TAKE1("OutputSed", sed_add_expr, |
|
511 (void *) APR_OFFSETOF(sed_config, output), |
|
512 ACCESS_CONF, |
|
513 "Sed regular expression for Response"), |
|
514 AP_INIT_TAKE1("InputSed", sed_add_expr, |
|
515 (void *) APR_OFFSETOF(sed_config, input), |
|
516 ACCESS_CONF, |
|
517 "Sed regular expression for Request"), |
|
518 {NULL} |
|
519 }; |
|
520 |
|
521 static void register_hooks(apr_pool_t *p) |
|
522 { |
|
523 ap_register_output_filter(sed_filter_name, sed_response_filter, NULL, |
|
524 AP_FTYPE_RESOURCE); |
|
525 ap_register_input_filter(sed_filter_name, sed_request_filter, NULL, |
|
526 AP_FTYPE_RESOURCE); |
|
527 } |
|
528 |
|
529 module AP_MODULE_DECLARE_DATA sed_module = { |
|
530 STANDARD20_MODULE_STUFF, |
|
531 create_sed_dir_config, /* dir config creater */ |
|
532 NULL, /* dir merger --- default is to override */ |
|
533 NULL, /* server config */ |
|
534 NULL, /* merge server config */ |
|
535 sed_filter_cmds, /* command table */ |
|
536 register_hooks /* register hooks */ |
|
537 }; |