]> arthur.barton.de Git - netatalk.git/blob - etc/psf/psf.c
Remove bdb env on exit
[netatalk.git] / etc / psf / psf.c
1 /*
2  * $Id: psf.c,v 1.13 2009-10-16 01:50:50 didg Exp $
3  *
4  * Copyright (c) 1990,1995 Regents of The University of Michigan.
5  * All Rights Reserved. See COPYRIGHT.
6  *
7  * PostScript Filter, psf.
8  *
9  * Handles both PostScript files and text files.  Files with the
10  * '%!' PostScript header are sent directly to the printer,
11  * unmodified.  Text files are first converted to PostScript,
12  * then sent.  Printers may be directly attached or on an AppleTalk
13  * network.  Other media are possible.  Currently, psf invokes
14  * pap to send files to AppleTalk-ed printers.  Replace the pap*
15  * variables to use another program for communication.  psf only
16  * converts plain-text.  If called as "tf" or "df", psf will invoke
17  * a troff or dvi to PostScript converter.
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #include "config.h"
22 #endif /* HAVE_CONFIG_H */
23
24 #define FUCKED
25
26 #ifdef HAVE_UNISTD_H
27 #include <unistd.h>
28 #endif /* HAVE_UNISTD_H */
29 #include <sys/time.h>
30
31 /* POSIX.1 sys/wait.h check */
32 #include <sys/types.h>
33 #ifdef HAVE_SYS_WAIT_H
34 #include <sys/wait.h>
35 #endif /* HAVE_SYS_WAIT_H */
36 #ifndef WEXITSTATUS
37 #define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8)
38 #endif /* ! WEXITSTATUS */
39 #ifndef WIFEXITED
40 #define WIFEXITED(stat_val) (((stat_val) & 255) == 0)
41 #endif /* ! WIFEXITED */
42
43 #include <sys/file.h>
44 #include <syslog.h>
45 #include <atalk/paths.h>
46 #include <stdio.h>
47 #include <stdlib.h>
48 #include <string.h>
49 #include <ctype.h>
50 #include <signal.h>
51 #include <errno.h>
52
53 /* Forward Declarations */
54 int pexecv(char *path, char *argv[]);
55 int copyio();
56 int textps();
57
58 static char             psapath[] = _PATH_PSA;
59 static char             *psaargv[] = { "psa", NULL, NULL, NULL, NULL };
60
61 /*
62  * If we're not doing accounting, we just call pap as below.
63  * If we are doing accounting, we call pap twice.  The first time,
64  * we call it with "-E" in arg 2, pagecount.ps in arg 3, and "-" in
65  * arg 4.  The second time, we call it with "-c" in arg 2, pagecount.ps
66  * in arg 3, and 0 in arg 4.
67  */
68 static char             pappath[] = _PATH_PAP;
69 static char             *papargv[] = { "pap", "-sstatus", NULL, NULL, NULL, NULL, NULL, NULL };
70
71 static char             revpath[] = _PATH_PSORDER;
72 static char             *revargv[] = { "psorder", "-d", NULL };
73
74 static char             *filtargv[] = { NULL, NULL, NULL };
75
76 static char             inbuf[ 1024 * 8 ];
77 static int              inlen;
78
79 static int              literal;
80 static int              width = 80, length = 66, indent = 0;
81 static char             *prog, *name, *host;
82
83 static struct papersize {
84     int         width;
85     int         length;
86    float        win;
87     float       lin;
88 } papersizes[] = {
89     { 80, 66, 8.5, 11.0 },                      /* US Letter */
90     { 80, 70, 8.27, 11.69 },                    /* A4 */
91 };
92
93 int main( int ac, char **av)
94 {
95     int                 c, rc, children = 0;
96 #ifdef FUCKED
97     int                 psafileno = 0, multiconn = 0, waitidle = 0, waitidle2 = 0;
98 #endif /* FUCKED */
99     int                 status;
100     extern char         *optarg;
101     extern int          optind, opterr;
102
103     opterr = 0;
104     if (( prog = rindex( av[ 0 ], '/' )) == NULL ) {
105         prog = av[ 0 ];
106     } else {
107         prog++;
108     }
109 #ifdef ultrix
110     openlog( prog, LOG_PID );
111 #else /* ultrix */
112     openlog( prog, LOG_PID, LOG_LPR );
113 #endif /* ultrix */
114
115     while (( c = getopt( ac, av, "P:C:D:F:L:J:x:y:n:h:w:l:i:c" )) != EOF ) {
116         switch ( c ) {
117         case 'n' :
118             name = optarg;
119             break;
120
121         case 'h' :
122             host = optarg;
123             break;
124
125         case 'w' :
126             width = atoi( optarg );
127 #ifdef ZEROWIDTH
128             /*
129              * Some version of lpd pass 0 for the page width.
130              */
131             if ( width == 0 ) {
132                 width = 80;
133             }
134 #endif /* ZEROWIDTH */
135             break;
136
137         case 'l' :
138             length = atoi( optarg );
139             break;
140
141         case 'i' :
142             indent = atoi( optarg );
143             break;
144
145         case 'c' :      /* Print control chars */
146             literal++;
147             break;
148
149         case 'F' :
150         case 'L' :
151         case 'J' :
152         case 'P' :
153         case 'x' :
154         case 'y' :
155             break;
156         
157 #ifdef notdef
158         default :
159             syslog( LOG_ERR, "bad option: %c", c );
160             exit( 2 );
161 #endif /* notdef */
162         }
163     }
164     if ( ac - optind > 1 ) {
165         syslog( LOG_ERR, "Too many arguments" );
166         exit( 2 );
167     }
168 #ifdef FUCKED
169     if ( index( prog, 'w' )) {
170         waitidle++;
171     }
172     if ( index( prog, 'W' )) {
173         waitidle2++;
174     }
175     if ( index( prog, 'm' )) {
176         multiconn++;
177     }
178 #endif /* FUCKED */
179
180     syslog( LOG_INFO, "starting for %s", name ? name : "?" );
181
182 restart:
183     if (( inlen = read( 0, inbuf, sizeof( inbuf ))) < 0 ) {
184         syslog( LOG_ERR, "read: %s", strerror(errno) );
185         exit( 1 );
186     }
187     if ( inlen == 0 ) { /* nothing to be done */
188         syslog( LOG_INFO, "done" );
189         exit( 0 );
190     }
191
192     /*
193      * If we've been given an accounting file, start the accounting
194      * process.
195      */
196     if ( optind < ac ) {
197         /* build arguments */
198         psaargv[ 1 ] = av[ optind ];
199         psaargv[ 2 ] = name;
200         psaargv[ 3 ] = host;
201         if (( c = pexecv( psapath, psaargv )) < 0 ) {
202             syslog( LOG_ERR, "%s: %s", psapath, strerror(errno) );
203             exit( 2 );
204         }
205         children++;
206         syslog( LOG_INFO, "accounting with psa[%d]", c );
207     }
208
209     /*
210      * Check prog's name to decide what programs to execute.
211      */
212     if ( strstr( prog, "pap" ) != NULL ) {
213         if ( optind < ac ) {    /* accounting */
214 #ifdef FUCKED
215             if ( multiconn ) {
216                 psafileno = getdtablesize();
217                 psafileno--;
218                 dup2( 1, psafileno );
219
220                 if ( waitidle2 ) {
221                     papargv[ 2 ] = "-W";
222                     papargv[ 3 ] = "-c";
223                     papargv[ 4 ] = "-E";
224                     papargv[ 5 ] = _PATH_PAGECOUNT;
225                     papargv[ 6 ] = "-";
226                     papargv[ 7 ] = NULL;
227                 } else if ( waitidle ) {
228                     papargv[ 2 ] = "-w";
229                     papargv[ 3 ] = "-c";
230                     papargv[ 4 ] = "-E";
231                     papargv[ 5 ] = _PATH_PAGECOUNT;
232                     papargv[ 6 ] = "-";
233                     papargv[ 7 ] = NULL;
234                 } else {
235                     papargv[ 2 ] = "-c";
236                     papargv[ 3 ] = "-E";
237                     papargv[ 4 ] = _PATH_PAGECOUNT;
238                     papargv[ 5 ] = "-";
239                     papargv[ 6 ] = NULL;
240                 }
241             } else {
242                 /*
243                  * This is how it should be done.
244                  */
245                 papargv[ 2 ] = "-c";
246                 papargv[ 3 ] = _PATH_PAGECOUNT;
247                 papargv[ 4 ] = "-";
248                 papargv[ 5 ] = _PATH_PAGECOUNT;
249                 papargv[ 6 ] = NULL;
250             }
251 #endif /* FUCKED */
252         } else {
253             papargv[ 2 ] = "-c";
254             papargv[ 3 ] = "-E";
255             papargv[ 4 ] = NULL;
256         }
257
258         if (( c = pexecv( pappath, papargv )) < 0 ) {
259             syslog( LOG_ERR, "%s: %s", pappath, strerror(errno) );
260             exit( 2 );
261         }
262         children++;
263         syslog( LOG_INFO, "sending to pap[%d]", c );
264     }
265
266     /*
267      * Might be a good idea to have both a "forw" and a "rev", so that
268      * reversed documents can be reordered for the printing device.
269      */
270     if ( strstr( prog, "rev" ) != NULL ) {
271         if (( c = pexecv( revpath, revargv )) < 0 ) {
272             syslog( LOG_ERR, "%s: %s", revpath, strerror(errno) );
273             exit( 2 );
274         }
275         syslog( LOG_INFO, "sending to rev[%d]", c );
276         children++;
277     }
278
279     /*
280      * Invoke an external (script) filter to produce PostScript from
281      * non-text input.
282      */
283     if ( *prog != 'i' && *prog != 'o' && *( prog + 1 ) == 'f' ) {
284         filtargv[ 0 ] = filtargv[ 1 ] = prog;
285         if (( c = pexecv( _PATH_PSFILTER, filtargv )) < 0 ) {
286             syslog( LOG_ERR, "%s: %s", _PATH_PSFILTER, strerror(errno) );
287             exit( 2 );
288         }
289         syslog( LOG_INFO, "external filter[%d]", c );
290         children++;
291         rc = copyio();          /* external filter */
292     } else {
293         if ( inlen >= 2 && inbuf[ 0 ] == '%' && inbuf[ 1 ] == '!' ) {
294             syslog( LOG_INFO, "PostScript" );
295             rc = copyio();      /* PostScript */
296         } else if ( inlen >= 2 && inbuf[ 0 ] == '\033' && inbuf[ 1 ] == '%' ) {
297             syslog( LOG_INFO, "PostScript w/PJL" );
298             rc = copyio();      /* PostScript */
299         } else {
300             syslog( LOG_INFO, "straight text" );
301             rc = textps();      /* straight text */
302         }
303     }
304
305 #ifdef FUCKED
306     if ( strstr( prog, "pap" ) != NULL && optind < ac && multiconn ) {
307         dup2( psafileno, 1 );
308         close( psafileno );
309         papargv[ 2 ] = "-c";
310         if ( waitidle2 ) {
311             papargv[ 3 ] = "-W";
312             papargv[ 4 ] = _PATH_PAGECOUNT;
313             papargv[ 5 ] = NULL;
314         } else if ( waitidle ) {
315             papargv[ 3 ] = "-w";
316             papargv[ 4 ] = _PATH_PAGECOUNT;
317             papargv[ 5 ] = NULL;
318         } else {
319             papargv[ 3 ] = _PATH_PAGECOUNT;
320             papargv[ 4 ] = NULL;
321         }
322
323         if (( c = pexecv( pappath, papargv )) < 0 ) {
324             syslog( LOG_ERR, "%s: %s", pappath, strerror(errno) );
325             exit( 2 );
326         }
327         children++;
328         syslog( LOG_INFO, "pagecount with pap[%d]", c );
329     }
330 #endif /* FUCKED */
331
332     if ( children ) {
333         close( 1 );
334     }
335     while ( children ) {
336         if (( c = wait3( &status, 0, NULL )) < 0 ) {
337             syslog( LOG_ERR, "wait3: %s", strerror(errno) );
338             exit( 1 );
339         }
340         if ( WIFEXITED( status )) {
341 #ifndef WEXITSTATUS
342 #define WEXITSTATUS(x)  ((x).w_status)
343 #endif /* WEXITSTATUS */
344             if ( WEXITSTATUS( status ) != 0 ) {
345                 syslog( LOG_ERR, "%d died with %d", c, WEXITSTATUS( status ));
346                 exit( WEXITSTATUS( status ));
347             } else {
348                 syslog( LOG_INFO, "%d done", c );
349                 children--;
350             }
351         } else {
352             syslog( LOG_ERR, "%d died badly", c );
353             exit( 1 );
354         }
355     }
356
357     if ( rc == 3 ) {
358         syslog( LOG_INFO, "pausing" );
359         kill( getpid(), SIGSTOP );
360         syslog( LOG_INFO, "restarting" );
361         goto restart;
362     }
363
364     syslog( LOG_INFO, "done" );
365     exit( rc );
366 }
367
368 int copyio(void)
369 {
370     /* implement the FSM needed to do the suspend. Note that
371      * the last characters will be \031\001 so don't worry
372      * Fun things: 1. \031\001 should not be written to output device
373      *  2. The \031 can be last char of one read, \001 first of next
374      *      - we need to write \031 if not followed by \001
375      */
376     struct timeval      tv;
377     fd_set              fdset;
378     int                 ctl = 0;
379
380 notdone:
381     do {
382         /*
383          * First, \031 and \001 *must* be the last things in the buffer
384          * (\001 can be the first thing in the next buffer).  There's no
385          * need to scan any of the intervening bytes.  Second, if there's
386          * more input, the escape sequence was bogus, and we should keep
387          * reading.
388          */
389         if ( inlen == 1 ) {
390             if ( ctl == 1 ) {
391                 if ( inbuf[ 0 ] == '\001' ) {
392                     ctl = 2;
393                     break;
394                 }
395                 if ( write( 1, "\031", 1 ) != 1 ) {
396                     syslog( LOG_ERR, "write: %s", strerror(errno) );
397                     return( 1 );
398                 }
399                 ctl = 0;
400             }
401
402             if ( inbuf[ 0 ] == '\031' ) {
403                 ctl = 1;
404             }
405
406         } else {
407             if ( ctl == 1 ) {
408                 if ( write( 1, "\031", 1 ) != 1 ) {
409                     syslog( LOG_ERR, "write: %s", strerror(errno) );
410                     return( 1 );
411                 }
412             }
413             ctl = 0;
414             if ( inbuf[ inlen - 2 ] == '\031' &&
415                     inbuf[ inlen - 1 ] == '\001' ) {
416                 ctl = 2;
417             } else if ( inbuf[ inlen - 1 ] == '\031' ) {
418                 ctl = 1;
419             }
420         }
421
422         inlen -= ctl;
423         if (( inlen > 0 ) && ( write( 1, inbuf, inlen ) != inlen )) {
424             syslog( LOG_ERR, "write: %s", strerror(errno) );
425             return( 1 );
426         }
427         if ( ctl == 2 ) {
428             break;
429         }
430     } while (( inlen = read( 0, inbuf, sizeof( inbuf ))) > 0 );
431
432     if ( ctl == 2 ) {
433         tv.tv_sec = 2;
434         tv.tv_usec = 0;
435         FD_ZERO( &fdset );
436         FD_SET( 0, &fdset );
437         if ( select( 1, &fdset, NULL, NULL, &tv ) != 0 ) {
438             if (( inlen = read( 0, inbuf, sizeof( inbuf ))) > 0 ) {
439                 goto notdone;
440             }
441         }
442     }
443
444     if ( inlen < 0 ) {
445         syslog( LOG_ERR, "read: %s", strerror(errno) );
446         return( 1 );
447     }
448
449     if ( ctl == 1 ) {
450         if ( write( 1, "\031", 1 ) != 1 ) {
451             syslog( LOG_ERR, "write: %s", strerror(errno) );
452             return( 1 );
453         }
454     } else if ( ctl == 2 ) {
455         return( 3 );
456     }
457     return( 0 );
458 }
459
460 static char             *font = "Courier";
461 static int              point = 11;
462
463 static char             pspro[] = "\
464 /GSV save def                                           % global VM\n\
465 /SP {\n\
466         /SV save def                                    % save vmstate\n\
467         dup /H exch def                                 % save font height\n\
468         exch findfont exch scalefont setfont            % select font\n\
469         ( ) stringwidth pop /W exch def                 % save font width\n\
470         0.5 sub 72 mul /CY exch def                     % save start Y\n\
471         pop 0.5 add 72 mul /CX exch def                 % save start X\n\
472         CX CY moveto                                    % make current point\n\
473 } bind def\n\
474 /S /show load def\n\
475 /NL { CX CY H sub dup /CY exch def moveto } bind def\n\
476 /CR { CX CY moveto } bind def\n\
477 /B { W neg 0 rmoveto}bind def\n\
478 /T { W mul 0 rmoveto}bind def\n\
479 /EP { SV restore showpage } bind def\n\
480 %%EndProlog\n";
481
482 int textps(void)
483 {
484     struct papersize    papersize;
485     int                 state = 0, line = 0, col = 0, npages = 0, rc;
486     unsigned int        i;
487     char                *p, *end;
488
489 #define elements(x)     (sizeof(x)/sizeof((x)[0]))
490     for ( i = 0; i < elements( papersizes ); i++ ) {
491         if ( width == papersizes[ 0 ].width &&
492                 length == papersizes[ 0 ].length ) {
493             papersize = papersizes[ i ];
494             break;
495         }
496     }
497     if ( i >= elements( papersizes )) {
498         papersize = papersizes[ 0 ];            /* default */
499     }
500
501 #define ST_AVAIL                (1<<0)
502 #define ST_CONTROL              (1<<1)
503 #define ST_PAGE                 (1<<2)
504     /*
505      * convert text lines to postscript.
506      * A grungy little state machine. If I was more creative, I could
507      * probably think of a better way of doing this...
508      */
509     do {
510         p = inbuf;
511         end = inbuf + inlen;
512         while ( p < end ) {
513             if (( state & ST_PAGE ) == 0 && *p != '\031' && *p != '\001' ) {
514                 if ( npages == 0 ) {
515                     printf( "%%!PS-Adobe-2.0\n%%%%Pages: (atend)\n" );
516                     printf( "%%%%DocumentFonts: %s\n", font );
517                     fflush( stdout );
518
519                     /* output postscript prologue: */
520                     if ( write( 1, pspro, sizeof( pspro ) - 1 ) !=
521                             sizeof( pspro ) - 1 ) {
522                         syslog( LOG_ERR, "write prologue: %s", strerror(errno) );
523                         return( 1 );
524                     }
525                     if ( name && host ) {
526                         printf( "statusdict /jobname (%s@%s) put\n", name,
527                                 host );
528                     }
529                 }
530
531                 printf( "%%%%Page: ? %d\n", ++npages );
532                 printf( "%d %f %f /%s %d SP\n", indent,
533                         papersize.win, papersize.lin, font, point );
534                 state |= ST_PAGE;
535             }
536             if ( state & ST_CONTROL && *p != '\001' ) {
537                 /* It is a very bad thing to toss a job because it contains
538                  * unprintable characters.  Instead, we will convert them to
539                  * question marks.  This is adapted from a solution described
540                  * by Werner Eugster <eugster@giub.unibe.ch> on his ApplePrint
541                  * webpage (http://www.giub.unibe.ch/~eugster/appleprint.html).
542                  *
543                  * Note that this is rather ugly code.  The same change is
544                  * applied identically at two different locations in this file.
545                  * It would be better someday to combine the two.
546                 */
547                 if ( !literal ) {
548                         fprintf( stderr,
549                                 "unprintable character (0x%x) converted to ?!\n",
550                                 (unsigned char)*p );
551                         putchar( '?' ); /* Replace unprintable char with a question mark. */
552                 } else {
553                         printf( "\\%o", (unsigned char)031 );
554                 }
555                 state &= ~ST_CONTROL;
556                 col++;
557             }
558
559             switch ( *p ) {
560             case '\n' :         /* end of line */
561                 if ( state & ST_AVAIL ) {
562                     printf( ")S\n" );
563                     state &= ~ST_AVAIL;
564                 }
565                 printf( "NL\n" );
566                 line++;
567                 col = 0;
568                 if ( line >= length ) {
569                     printf( "EP\n" );
570                     state &= ~ST_PAGE;
571                     line = 0;
572                 }
573                 break;
574
575             case '\r' :         /* carriage return (for overtyping) */
576                 if ( state & ST_AVAIL ) {
577                     printf( ")S CR\n" );
578                     state &= ~ST_AVAIL;
579                 }
580                 col = 0;
581                 break;
582
583             case '\f' :         /* form feed */
584                 if ( state & ST_AVAIL ) {
585                     printf( ")S\n" );
586                     state &= ~ST_AVAIL;
587                 }
588                 printf( "EP\n" );
589                 state &= ~ST_PAGE;
590                 line = 0;
591                 col = 0;
592                 break;
593
594             case '\b' :         /* backspace */
595                 /* show line, back up one character */
596                 if ( state & ST_AVAIL ) {
597                     printf( ")S\n" );
598                     state &= ~ST_AVAIL;
599                 }
600                 printf( "B\n" );
601                 col--;
602                 break;
603
604             case '\t' :         /* tab */
605                 if ( state & ST_AVAIL ) {
606                     printf( ")S\n" );
607                     state &= ~ST_AVAIL;
608                 }
609                 printf( "%d T\n", 8 - ( col % 8 ));
610                 col += 8 - ( col % 8 );
611                 break;
612
613             /*
614              * beginning of lpr control sequence
615              */
616             case '\031' :
617                 state |= ST_CONTROL;
618                 break;
619
620             case '\001' :       /* lpr control sequence */
621                 if ( state & ST_CONTROL ) {
622                     rc = 3;
623                     goto out;
624                 }
625                 /* FALLTHROUGH */
626
627             case '\\' :
628             case ')' :
629             case '(' :
630                 if (( state & ST_AVAIL ) == 0 ) {
631                     printf( "(" );
632                     state |= ST_AVAIL;
633                 }
634                 putchar( '\\' );
635                 /* FALLTHROUGH */
636
637             default :
638                 if (( state & ST_AVAIL ) == 0 ) {
639                     printf( "(" );
640                     state |= ST_AVAIL;
641                 }
642                 if ( !isascii( *p ) || !isprint( *p )) {
643                     if ( !literal ) {
644                         fprintf( stderr,
645                             "unprintable character (0x%x) converted to ?!\n",
646                             (unsigned char)*p );
647                         putchar( '?' ); /* Replace unprintable char with a question mark. */
648                     } else {
649                         printf( "\\%o", (unsigned char)*p );
650                     }
651                 } else {
652                     putchar( *p );
653                 }
654                 col++;
655                 break;
656             }
657         p++;
658         }
659     } while (( inlen = read( 0, inbuf, sizeof( inbuf ))) > 0 );
660     if ( inlen < 0 ) {
661         syslog( LOG_ERR, "read: %s", strerror(errno) );
662         return( 1 );
663     }
664     rc = 0;
665
666 out:
667     if ( state & ST_AVAIL ) {
668         printf( ")S\n" );
669         state &= ~ST_AVAIL;
670     }
671
672     if ( state & ST_PAGE ) {
673         printf( "EP\n" );
674         state &= ~ST_PAGE;
675     }
676
677     if ( npages > 0 ) {
678         printf( "%%%%Trailer\nGSV restore\n%%%%Pages: %d\n%%%%EOF\n", npages );
679         fflush( stdout );
680     }
681
682     return( rc );
683 }
684
685 /*
686  * Interface to pipe and exec, for starting children in pipelines.
687  *
688  * Manipulates file descriptors 0, 1, and 2, such that the new child
689  * is reading from the parent's output.
690  */
691 int pexecv( char *path, char *argv[])
692 {
693     int         fd[ 2 ], c;
694
695     if ( pipe( fd ) < 0 ) {
696         return( -1 );
697     }
698
699     switch ( c = fork()) {
700     case -1 :
701         return( -1 );
702         /* NOTREACHED */
703
704     case 0 :
705         if ( close( fd[ 1 ] ) < 0 ) {
706             return( -1 );
707         }
708         if ( dup2( fd[ 0 ], 0 ) < 0 ) {
709             return( -1 );
710         }
711         if ( close( fd[ 0 ] ) < 0 ) {
712             return( -1 );
713         }
714         execv( path, argv );
715         return( -1 );
716         /* NOTREACHED */
717
718     default :
719         if ( close( fd[ 0 ] ) < 0 ) {
720             return( -1 );
721         }
722         if ( dup2( fd[ 1 ], 1 ) < 0 ) {
723             return( -1 );
724         }
725         if ( close( fd[ 1 ] ) < 0 ) {
726             return( -1 );
727         }
728         return( c );
729     }
730 }