]> Dogcows Code - chaz/tar/blob - src/incremen.c
(directory.new): New member
[chaz/tar] / src / incremen.c
1 /* GNU dump extensions to tar.
2
3 Copyright (C) 1988, 1992, 1993, 1994, 1996, 1997, 1999, 2000, 2001,
4 2003, 2004, 2005, 2006 Free Software Foundation, Inc.
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any later
9 version.
10
11 This program is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
14 Public License for more details.
15
16 You should have received a copy of the GNU General Public License along
17 with this program; if not, write to the Free Software Foundation, Inc.,
18 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */
19
20 #include <system.h>
21 #include <getline.h>
22 #include <hash.h>
23 #include <quotearg.h>
24 #include "common.h"
25
26 /* Incremental dump specialities. */
27
28 /* Which child files to save under a directory. */
29 enum children {NO_CHILDREN, CHANGED_CHILDREN, ALL_CHILDREN};
30
31 /* Directory attributes. */
32 struct directory
33 {
34 struct timespec mtime; /* Modification time */
35 dev_t device_number; /* device number for directory */
36 ino_t inode_number; /* inode number for directory */
37 enum children children; /* what to save under this directory */
38 bool nfs; /* is the directory mounted on nfs? */
39 bool found; /* was the directory found on fs? */
40 bool new; /* is it new? */
41 char name[1]; /* file name of directory */
42 };
43
44 static Hash_table *directory_table;
45
46 #if HAVE_ST_FSTYPE_STRING
47 static char const nfs_string[] = "nfs";
48 # define NFS_FILE_STAT(st) (strcmp ((st).st_fstype, nfs_string) == 0)
49 #else
50 # define ST_DEV_MSB(st) (~ (dev_t) 0 << (sizeof (st).st_dev * CHAR_BIT - 1))
51 # define NFS_FILE_STAT(st) (((st).st_dev & ST_DEV_MSB (st)) != 0)
52 #endif
53
54 /* Calculate the hash of a directory. */
55 static size_t
56 hash_directory (void const *entry, size_t n_buckets)
57 {
58 struct directory const *directory = entry;
59 return hash_string (directory->name, n_buckets);
60 }
61
62 /* Compare two directories for equality. */
63 static bool
64 compare_directories (void const *entry1, void const *entry2)
65 {
66 struct directory const *directory1 = entry1;
67 struct directory const *directory2 = entry2;
68 return strcmp (directory1->name, directory2->name) == 0;
69 }
70
71 static struct directory *
72 make_directory (const char *name)
73 {
74 size_t namelen = strlen (name);
75 size_t size = offsetof (struct directory, name) + namelen + 1;
76 struct directory *directory = xmalloc (size);
77 strcpy (directory->name, name);
78 if (ISSLASH (directory->name[namelen-1]))
79 directory->name[namelen-1] = 0;
80 directory->new = false;
81 return directory;
82 }
83
84 /* Create and link a new directory entry for directory NAME, having a
85 device number DEV and an inode number INO, with NFS indicating
86 whether it is an NFS device and FOUND indicating whether we have
87 found that the directory exists. */
88 static struct directory *
89 note_directory (char const *name, struct timespec mtime,
90 dev_t dev, ino_t ino, bool nfs, bool found)
91 {
92 struct directory *directory = make_directory (name);
93
94 directory->mtime = mtime;
95 directory->device_number = dev;
96 directory->inode_number = ino;
97 directory->children = CHANGED_CHILDREN;
98 directory->nfs = nfs;
99 directory->found = found;
100
101 if (! ((directory_table
102 || (directory_table = hash_initialize (0, 0, hash_directory,
103 compare_directories, 0)))
104 && hash_insert (directory_table, directory)))
105 xalloc_die ();
106
107 return directory;
108 }
109
110 /* Return a directory entry for a given file NAME, or zero if none found. */
111 static struct directory *
112 find_directory (char *name)
113 {
114 if (! directory_table)
115 return 0;
116 else
117 {
118 struct directory *dir = make_directory (name);
119 struct directory *ret = hash_lookup (directory_table, dir);
120 free (dir);
121 return ret;
122 }
123 }
124
125 void
126 update_parent_directory (const char *name)
127 {
128 struct directory *directory;
129 char *p, *name_buffer;
130
131 p = dir_name (name);
132 directory = find_directory (p);
133 if (directory)
134 {
135 struct stat st;
136 if (deref_stat (dereference_option, p, &st) != 0)
137 stat_diag (name);
138 else
139 directory->mtime = get_stat_mtime (&st);
140 }
141 free (p);
142 }
143
144 static int
145 compare_dirents (const void *first, const void *second)
146 {
147 return strcmp ((*(char *const *) first) + 1,
148 (*(char *const *) second) + 1);
149 }
150
151 enum children
152 procdir (char *name_buffer, struct stat *stat_data,
153 dev_t device,
154 enum children children,
155 bool verbose)
156 {
157 struct directory *directory;
158 bool nfs = NFS_FILE_STAT (*stat_data);
159 struct name *np;
160
161 if ((directory = find_directory (name_buffer)) != NULL)
162 {
163 /* With NFS, the same file can have two different devices
164 if an NFS directory is mounted in multiple locations,
165 which is relatively common when automounting.
166 To avoid spurious incremental redumping of
167 directories, consider all NFS devices as equal,
168 relying on the i-node to establish differences. */
169
170 if (! (((directory->nfs & nfs)
171 || directory->device_number == stat_data->st_dev)
172 && directory->inode_number == stat_data->st_ino))
173 {
174 if (verbose)
175 WARN ((0, 0, _("%s: Directory has been renamed"),
176 quotearg_colon (name_buffer)));
177 directory->children = ALL_CHILDREN;
178 directory->nfs = nfs;
179 directory->device_number = stat_data->st_dev;
180 directory->inode_number = stat_data->st_ino;
181 }
182 else if (listed_incremental_option && !directory->new)
183 /* Newer modification time can mean that new files were
184 created in the directory or some of the existing files
185 were renamed. */
186 directory->children =
187 timespec_cmp (get_stat_mtime (stat_data), directory->mtime) > 0
188 ? ALL_CHILDREN : CHANGED_CHILDREN;
189
190 directory->found = true;
191 }
192 else
193 {
194 if (verbose)
195 WARN ((0, 0, _("%s: Directory is new"),
196 quotearg_colon (name_buffer)));
197 directory = note_directory (name_buffer,
198 get_stat_mtime(stat_data),
199 stat_data->st_dev,
200 stat_data->st_ino,
201 nfs,
202 true);
203
204 directory->children =
205 (listed_incremental_option
206 || (OLDER_STAT_TIME (*stat_data, m)
207 || (after_date_option
208 && OLDER_STAT_TIME (*stat_data, c))))
209 ? ALL_CHILDREN
210 : CHANGED_CHILDREN;
211 directory->new = true;
212 }
213
214 /* If the directory is on another device and --one-file-system was given,
215 omit it... */
216 if (one_file_system_option && device != stat_data->st_dev
217 /* ... except if it was explicitely given in the command line */
218 && !((np = name_scan (name_buffer, true)) && np->explicit))
219 directory->children = NO_CHILDREN;
220 else if (children == ALL_CHILDREN)
221 directory->children = ALL_CHILDREN;
222
223 return directory->children;
224 }
225
226
227 /* Recursively scan the given directory. */
228 static void
229 scan_directory (struct obstack *stk, char *dir_name, dev_t device)
230 {
231 char *dirp = savedir (dir_name); /* for scanning directory */
232 char const *entry; /* directory entry being scanned */
233 size_t entrylen; /* length of directory entry */
234 char *name_buffer; /* directory, `/', and directory member */
235 size_t name_buffer_size; /* allocated size of name_buffer, minus 2 */
236 size_t name_length; /* used length in name_buffer */
237 enum children children;
238 struct stat stat_data;
239
240 if (! dirp)
241 savedir_error (dir_name);
242
243 name_buffer_size = strlen (dir_name) + NAME_FIELD_SIZE;
244 name_buffer = xmalloc (name_buffer_size + 2);
245 strcpy (name_buffer, dir_name);
246 if (! ISSLASH (dir_name[strlen (dir_name) - 1]))
247 strcat (name_buffer, "/");
248 name_length = strlen (name_buffer);
249
250 if (deref_stat (dereference_option, name_buffer, &stat_data))
251 {
252 stat_diag (name_buffer);
253 children = CHANGED_CHILDREN;
254 }
255 else
256 children = procdir (name_buffer, &stat_data, device, NO_CHILDREN, false);
257
258 if (dirp && children != NO_CHILDREN)
259 for (entry = dirp;
260 (entrylen = strlen (entry)) != 0;
261 entry += entrylen + 1)
262 {
263 if (name_buffer_size <= entrylen + name_length)
264 {
265 do
266 name_buffer_size += NAME_FIELD_SIZE;
267 while (name_buffer_size <= entrylen + name_length);
268 name_buffer = xrealloc (name_buffer, name_buffer_size + 2);
269 }
270 strcpy (name_buffer + name_length, entry);
271
272 if (excluded_name (name_buffer))
273 obstack_1grow (stk, 'N');
274 else
275 {
276
277 if (deref_stat (dereference_option, name_buffer, &stat_data))
278 {
279 stat_diag (name_buffer);
280 continue;
281 }
282
283 if (S_ISDIR (stat_data.st_mode))
284 {
285 procdir (name_buffer, &stat_data, device, children,
286 verbose_option);
287 obstack_1grow (stk, 'D');
288 }
289
290 else if (one_file_system_option && device != stat_data.st_dev)
291 obstack_1grow (stk, 'N');
292
293 #ifdef S_ISHIDDEN
294 else if (S_ISHIDDEN (stat_data.st_mode))
295 {
296 obstack_1grow (stk, 'D');
297 obstack_grow (stk, entry, entrylen);
298 obstack_grow (stk, "A", 2);
299 continue;
300 }
301 #endif
302
303 else if (children == CHANGED_CHILDREN
304 && OLDER_STAT_TIME (stat_data, m)
305 && (!after_date_option || OLDER_STAT_TIME (stat_data, c)))
306 obstack_1grow (stk, 'N');
307 else
308 obstack_1grow (stk, 'Y');
309 }
310
311 obstack_grow (stk, entry, entrylen + 1);
312 }
313
314 obstack_grow (stk, "\000\000", 2);
315
316 free (name_buffer);
317 if (dirp)
318 free (dirp);
319 }
320
321 /* Sort the contents of the obstack, and convert it to the char * */
322 static char *
323 sort_obstack (struct obstack *stk)
324 {
325 char *pointer = obstack_finish (stk);
326 size_t counter;
327 char *cursor;
328 char *buffer;
329 char **array;
330 char **array_cursor;
331
332 counter = 0;
333 for (cursor = pointer; *cursor; cursor += strlen (cursor) + 1)
334 counter++;
335
336 if (!counter)
337 return NULL;
338
339 array = obstack_alloc (stk, sizeof (char *) * (counter + 1));
340
341 array_cursor = array;
342 for (cursor = pointer; *cursor; cursor += strlen (cursor) + 1)
343 *array_cursor++ = cursor;
344 *array_cursor = 0;
345
346 qsort (array, counter, sizeof (char *), compare_dirents);
347
348 buffer = xmalloc (cursor - pointer + 2);
349
350 cursor = buffer;
351 for (array_cursor = array; *array_cursor; array_cursor++)
352 {
353 char *string = *array_cursor;
354
355 while ((*cursor++ = *string++))
356 continue;
357 }
358 *cursor = '\0';
359 return buffer;
360 }
361
362 char *
363 get_directory_contents (char *dir_name, dev_t device)
364 {
365 struct obstack stk;
366 char *buffer;
367
368 obstack_init (&stk);
369 scan_directory (&stk, dir_name, device);
370 buffer = sort_obstack (&stk);
371 obstack_free (&stk, NULL);
372 return buffer;
373 }
374
375 size_t
376 dumpdir_size (const char *p)
377 {
378 size_t totsize = 0;
379
380 while (*p)
381 {
382 size_t size = strlen (p) + 1;
383 totsize += size;
384 p += size;
385 }
386 return totsize + 1;
387 }
388
389 \f
390
391 static FILE *listed_incremental_stream;
392
393 /* Version of incremental format snapshots (directory files) used by this
394 tar. Currently it is supposed to be a single decimal number. 0 means
395 incremental snapshots as per tar version before 1.15.2.
396
397 The current tar version supports incremental versions from
398 0 up to TAR_INCREMENTAL_VERSION, inclusive.
399 It is able to create only snapshots of TAR_INCREMENTAL_VERSION */
400
401 #define TAR_INCREMENTAL_VERSION 1
402
403 /* Read incremental snapshot file (directory file).
404 If the file has older incremental version, make sure that it is processed
405 correctly and that tar will use the most conservative backup method among
406 possible alternatives (i.e. prefer ALL_CHILDREN over CHANGED_CHILDREN,
407 etc.) This ensures that the snapshots are updated to the recent version
408 without any loss of data. */
409 void
410 read_directory_file (void)
411 {
412 int fd;
413 FILE *fp;
414 char *buf = 0;
415 size_t bufsize;
416
417 /* Open the file for both read and write. That way, we can write
418 it later without having to reopen it, and don't have to worry if
419 we chdir in the meantime. */
420 fd = open (listed_incremental_option, O_RDWR | O_CREAT, MODE_RW);
421 if (fd < 0)
422 {
423 open_error (listed_incremental_option);
424 return;
425 }
426
427 fp = fdopen (fd, "r+");
428 if (! fp)
429 {
430 open_error (listed_incremental_option);
431 close (fd);
432 return;
433 }
434
435 listed_incremental_stream = fp;
436
437 if (0 < getline (&buf, &bufsize, fp))
438 {
439 char *ebuf;
440 int n;
441 long lineno = 1;
442 uintmax_t u;
443 time_t t = u;
444 int incremental_version;
445
446 if (strncmp (buf, PACKAGE_NAME, sizeof PACKAGE_NAME - 1) == 0)
447 {
448 ebuf = buf + sizeof PACKAGE_NAME - 1;
449 if (*ebuf++ != '-')
450 ERROR((1, 0, _("Bad incremental file format")));
451 for (; *ebuf != '-'; ebuf++)
452 if (!*ebuf)
453 ERROR((1, 0, _("Bad incremental file format")));
454
455 incremental_version = (errno = 0, strtoumax (ebuf+1, &ebuf, 10));
456 if (getline (&buf, &bufsize, fp) <= 0)
457 {
458 read_error (listed_incremental_option);
459 free (buf);
460 return;
461 }
462 ++lineno;
463 }
464 else
465 incremental_version = 0;
466
467 if (incremental_version > TAR_INCREMENTAL_VERSION)
468 ERROR((1, 0, _("Unsupported incremental format version: %d"),
469 incremental_version));
470
471 t = u = (errno = 0, strtoumax (buf, &ebuf, 10));
472 if (buf == ebuf || (u == 0 && errno == EINVAL))
473 ERROR ((0, 0, "%s:%ld: %s",
474 quotearg_colon (listed_incremental_option),
475 lineno,
476 _("Invalid time stamp")));
477 else if (t != u)
478 ERROR ((0, 0, "%s:%ld: %s",
479 quotearg_colon (listed_incremental_option),
480 lineno,
481 _("Time stamp out of range")));
482 else if (incremental_version == 1)
483 {
484 newer_mtime_option.tv_sec = t;
485
486 t = u = (errno = 0, strtoumax (buf, &ebuf, 10));
487 if (buf == ebuf || (u == 0 && errno == EINVAL))
488 ERROR ((0, 0, "%s:%ld: %s",
489 quotearg_colon (listed_incremental_option),
490 lineno,
491 _("Invalid time stamp")));
492 else if (t != u)
493 ERROR ((0, 0, "%s:%ld: %s",
494 quotearg_colon (listed_incremental_option),
495 lineno,
496 _("Time stamp out of range")));
497 newer_mtime_option.tv_nsec = t;
498 }
499 else
500 {
501 /* pre-1 incremental format does not contain nanoseconds */
502 newer_mtime_option.tv_sec = t;
503 newer_mtime_option.tv_nsec = 0;
504 }
505
506 while (0 < (n = getline (&buf, &bufsize, fp)))
507 {
508 dev_t dev;
509 ino_t ino;
510 bool nfs = buf[0] == '+';
511 char *strp = buf + nfs;
512 struct timespec mtime;
513
514 lineno++;
515
516 if (buf[n - 1] == '\n')
517 buf[n - 1] = '\0';
518
519 if (incremental_version == 1)
520 {
521 errno = 0;
522 mtime.tv_sec = u = strtoumax (strp, &ebuf, 10);
523 if (!isspace (*ebuf))
524 ERROR ((0, 0, "%s:%ld: %s",
525 quotearg_colon (listed_incremental_option), lineno,
526 _("Invalid modification time (seconds)")));
527 else if (mtime.tv_sec != u)
528 ERROR ((0, 0, "%s:%ld: %s",
529 quotearg_colon (listed_incremental_option), lineno,
530 _("Modification time (seconds) out of range")));
531 strp = ebuf;
532
533 errno = 0;
534 mtime.tv_nsec = u = strtoumax (strp, &ebuf, 10);
535 if (!isspace (*ebuf))
536 ERROR ((0, 0, "%s:%ld: %s",
537 quotearg_colon (listed_incremental_option), lineno,
538 _("Invalid modification time (nanoseconds)")));
539 else if (mtime.tv_nsec != u)
540 ERROR ((0, 0, "%s:%ld: %s",
541 quotearg_colon (listed_incremental_option), lineno,
542 _("Modification time (nanoseconds) out of range")));
543 strp = ebuf;
544 }
545 else
546 memset (&mtime, 0, sizeof mtime);
547
548 errno = 0;
549 dev = u = strtoumax (strp, &ebuf, 10);
550 if (!isspace (*ebuf))
551 ERROR ((0, 0, "%s:%ld: %s",
552 quotearg_colon (listed_incremental_option), lineno,
553 _("Invalid device number")));
554 else if (dev != u)
555 ERROR ((0, 0, "%s:%ld: %s",
556 quotearg_colon (listed_incremental_option), lineno,
557 _("Device number out of range")));
558 strp = ebuf;
559
560 errno = 0;
561 ino = u = strtoumax (strp, &ebuf, 10);
562 if (!isspace (*ebuf))
563 ERROR ((0, 0, "%s:%ld: %s",
564 quotearg_colon (listed_incremental_option), lineno,
565 _("Invalid inode number")));
566 else if (ino != u)
567 ERROR ((0, 0, "%s:%ld: %s",
568 quotearg_colon (listed_incremental_option), lineno,
569 _("Inode number out of range")));
570 strp = ebuf;
571
572 strp++;
573 unquote_string (strp);
574 note_directory (strp, mtime, dev, ino, nfs, false);
575 }
576 }
577
578 if (ferror (fp))
579 read_error (listed_incremental_option);
580 if (buf)
581 free (buf);
582 }
583
584 /* Output incremental data for the directory ENTRY to the file DATA.
585 Return nonzero if successful, preserving errno on write failure. */
586 static bool
587 write_directory_file_entry (void *entry, void *data)
588 {
589 struct directory const *directory = entry;
590 FILE *fp = data;
591
592 if (directory->found)
593 {
594 int e;
595 char buf[UINTMAX_STRSIZE_BOUND];
596 char *str = quote_copy_string (directory->name);
597
598 if (directory->nfs)
599 fprintf (fp, "+");
600 fprintf (fp, "%s ", umaxtostr (directory->mtime.tv_sec, buf));
601 fprintf (fp, "%s ", umaxtostr (directory->mtime.tv_nsec, buf));
602 fprintf (fp, "%s ", umaxtostr (directory->device_number, buf));
603 fprintf (fp, "%s ", umaxtostr (directory->inode_number, buf));
604 fprintf (fp, "%s\n", str ? str : directory->name);
605
606 e = errno;
607 if (str)
608 free (str);
609 errno = e;
610 }
611
612 return ! ferror (fp);
613 }
614
615 void
616 write_directory_file (void)
617 {
618 FILE *fp = listed_incremental_stream;
619
620 if (! fp)
621 return;
622
623 if (fseek (fp, 0L, SEEK_SET) != 0)
624 seek_error (listed_incremental_option);
625 if (sys_truncate (fileno (fp)) != 0)
626 truncate_error (listed_incremental_option);
627
628 fprintf (fp, "%s-%s-%d\n", PACKAGE_NAME, PACKAGE_VERSION,
629 TAR_INCREMENTAL_VERSION);
630
631 fprintf (fp, "%lu %lu\n",
632 (unsigned long int) start_time.tv_sec,
633 (unsigned long int) start_time.tv_nsec);
634 if (! ferror (fp) && directory_table)
635 hash_do_for_each (directory_table, write_directory_file_entry, fp);
636 if (ferror (fp))
637 write_error (listed_incremental_option);
638 if (fclose (fp) != 0)
639 close_error (listed_incremental_option);
640 }
641
642 \f
643 /* Restoration of incremental dumps. */
644
645 static void
646 get_gnu_dumpdir (struct tar_stat_info *stat_info)
647 {
648 size_t size;
649 size_t copied;
650 union block *data_block;
651 char *to;
652 char *archive_dir;
653
654 size = stat_info->stat.st_size;
655
656 archive_dir = xmalloc (size);
657 to = archive_dir;
658
659 set_next_block_after (current_header);
660 mv_begin (stat_info);
661
662 for (; size > 0; size -= copied)
663 {
664 mv_size_left (size);
665 data_block = find_next_block ();
666 if (!data_block)
667 ERROR ((1, 0, _("Unexpected EOF in archive")));
668 copied = available_space_after (data_block);
669 if (copied > size)
670 copied = size;
671 memcpy (to, data_block->buffer, copied);
672 to += copied;
673 set_next_block_after ((union block *)
674 (data_block->buffer + copied - 1));
675 }
676
677 mv_end ();
678
679 stat_info->dumpdir = archive_dir;
680 stat_info->skipped = true; /* For skip_member() and friends
681 to work correctly */
682 }
683
684 /* Return T if STAT_INFO represents a dumpdir archive member.
685 Note: can invalidate current_header. It happens if flush_archive()
686 gets called within get_gnu_dumpdir() */
687 bool
688 is_dumpdir (struct tar_stat_info *stat_info)
689 {
690 if (stat_info->is_dumpdir && !stat_info->dumpdir)
691 get_gnu_dumpdir (stat_info);
692 return stat_info->is_dumpdir;
693 }
694
695 /* Examine the directories under directory_name and delete any
696 files that were not there at the time of the back-up. */
697 void
698 purge_directory (char const *directory_name)
699 {
700 char *current_dir;
701 char *cur, *arc;
702
703 if (!is_dumpdir (&current_stat_info))
704 {
705 skip_member ();
706 return;
707 }
708
709 current_dir = savedir (directory_name);
710
711 if (!current_dir)
712 {
713 /* The directory doesn't exist now. It'll be created. In any
714 case, we don't have to delete any files out of it. */
715
716 skip_member ();
717 return;
718 }
719
720 for (cur = current_dir; *cur; cur += strlen (cur) + 1)
721 {
722 for (arc = current_stat_info.dumpdir; *arc; arc += strlen (arc) + 1)
723 {
724 arc++;
725 if (!strcmp (arc, cur))
726 break;
727 }
728 if (*arc == '\0')
729 {
730 struct stat st;
731 char *p = new_name (directory_name, cur);
732
733 if (deref_stat (false, p, &st))
734 {
735 stat_diag (p);
736 WARN((0, 0, _("%s: Not purging directory: unable to stat"),
737 quotearg_colon (p)));
738 continue;
739 }
740 else if (one_file_system_option && st.st_dev != root_device)
741 {
742 WARN((0, 0,
743 _("%s: directory is on a different device: not purging"),
744 quotearg_colon (p)));
745 continue;
746 }
747
748 if (! interactive_option || confirm ("delete", p))
749 {
750 if (verbose_option)
751 fprintf (stdlis, _("%s: Deleting %s\n"),
752 program_name, quote (p));
753 if (! remove_any_file (p, RECURSIVE_REMOVE_OPTION))
754 {
755 int e = errno;
756 ERROR ((0, e, _("%s: Cannot remove"), quotearg_colon (p)));
757 }
758 }
759 free (p);
760 }
761
762 }
763 free (current_dir);
764 }
765
766 void
767 list_dumpdir (char *buffer, size_t size)
768 {
769 while (size)
770 {
771 switch (*buffer)
772 {
773 case 'Y':
774 case 'N':
775 case 'D':
776 fprintf (stdlis, "%c ", *buffer);
777 buffer++;
778 size--;
779 break;
780
781 case 0:
782 fputc ('\n', stdlis);
783 buffer++;
784 size--;
785 break;
786
787 default:
788 fputc (*buffer, stdlis);
789 buffer++;
790 size--;
791 }
792 }
793 }
This page took 0.06887 seconds and 5 git commands to generate.