]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/basic/fileio.c
fileio: add READ_FULL_FILE_UNBASE64 flag for read_full_file_full()
[thirdparty/systemd.git] / src / basic / fileio.c
1 /* SPDX-License-Identifier: LGPL-2.1+ */
2
3 #include <ctype.h>
4 #include <errno.h>
5 #include <fcntl.h>
6 #include <limits.h>
7 #include <stdarg.h>
8 #include <stdint.h>
9 #include <stdio_ext.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <sys/stat.h>
13 #include <sys/types.h>
14 #include <unistd.h>
15
16 #include "alloc-util.h"
17 #include "fd-util.h"
18 #include "fileio.h"
19 #include "fs-util.h"
20 #include "hexdecoct.h"
21 #include "log.h"
22 #include "macro.h"
23 #include "missing.h"
24 #include "parse-util.h"
25 #include "path-util.h"
26 #include "stdio-util.h"
27 #include "string-util.h"
28 #include "tmpfile-util.h"
29
30 #define READ_FULL_BYTES_MAX (4U*1024U*1024U)
31
32 int write_string_stream_ts(
33 FILE *f,
34 const char *line,
35 WriteStringFileFlags flags,
36 struct timespec *ts) {
37
38 bool needs_nl;
39 int r;
40
41 assert(f);
42 assert(line);
43
44 if (ferror(f))
45 return -EIO;
46
47 needs_nl = !(flags & WRITE_STRING_FILE_AVOID_NEWLINE) && !endswith(line, "\n");
48
49 if (needs_nl && (flags & WRITE_STRING_FILE_DISABLE_BUFFER)) {
50 /* If STDIO buffering was disabled, then let's append the newline character to the string itself, so
51 * that the write goes out in one go, instead of two */
52
53 line = strjoina(line, "\n");
54 needs_nl = false;
55 }
56
57 if (fputs(line, f) == EOF)
58 return -errno;
59
60 if (needs_nl)
61 if (fputc('\n', f) == EOF)
62 return -errno;
63
64 if (flags & WRITE_STRING_FILE_SYNC)
65 r = fflush_sync_and_check(f);
66 else
67 r = fflush_and_check(f);
68 if (r < 0)
69 return r;
70
71 if (ts) {
72 struct timespec twice[2] = {*ts, *ts};
73
74 if (futimens(fileno(f), twice) < 0)
75 return -errno;
76 }
77
78 return 0;
79 }
80
81 static int write_string_file_atomic(
82 const char *fn,
83 const char *line,
84 WriteStringFileFlags flags,
85 struct timespec *ts) {
86
87 _cleanup_fclose_ FILE *f = NULL;
88 _cleanup_free_ char *p = NULL;
89 int r;
90
91 assert(fn);
92 assert(line);
93
94 r = fopen_temporary(fn, &f, &p);
95 if (r < 0)
96 return r;
97
98 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
99 (void) fchmod_umask(fileno(f), 0644);
100
101 r = write_string_stream_ts(f, line, flags, ts);
102 if (r < 0)
103 goto fail;
104
105 if (rename(p, fn) < 0) {
106 r = -errno;
107 goto fail;
108 }
109
110 return 0;
111
112 fail:
113 (void) unlink(p);
114 return r;
115 }
116
117 int write_string_file_ts(
118 const char *fn,
119 const char *line,
120 WriteStringFileFlags flags,
121 struct timespec *ts) {
122
123 _cleanup_fclose_ FILE *f = NULL;
124 int q, r;
125
126 assert(fn);
127 assert(line);
128
129 /* We don't know how to verify whether the file contents was already on-disk. */
130 assert(!((flags & WRITE_STRING_FILE_VERIFY_ON_FAILURE) && (flags & WRITE_STRING_FILE_SYNC)));
131
132 if (flags & WRITE_STRING_FILE_ATOMIC) {
133 assert(flags & WRITE_STRING_FILE_CREATE);
134
135 r = write_string_file_atomic(fn, line, flags, ts);
136 if (r < 0)
137 goto fail;
138
139 return r;
140 } else
141 assert(!ts);
142
143 if (flags & WRITE_STRING_FILE_CREATE) {
144 f = fopen(fn, "we");
145 if (!f) {
146 r = -errno;
147 goto fail;
148 }
149 } else {
150 int fd;
151
152 /* We manually build our own version of fopen(..., "we") that
153 * works without O_CREAT */
154 fd = open(fn, O_WRONLY|O_CLOEXEC|O_NOCTTY | ((flags & WRITE_STRING_FILE_NOFOLLOW) ? O_NOFOLLOW : 0));
155 if (fd < 0) {
156 r = -errno;
157 goto fail;
158 }
159
160 f = fdopen(fd, "w");
161 if (!f) {
162 r = -errno;
163 safe_close(fd);
164 goto fail;
165 }
166 }
167
168 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
169
170 if (flags & WRITE_STRING_FILE_DISABLE_BUFFER)
171 setvbuf(f, NULL, _IONBF, 0);
172
173 r = write_string_stream_ts(f, line, flags, ts);
174 if (r < 0)
175 goto fail;
176
177 return 0;
178
179 fail:
180 if (!(flags & WRITE_STRING_FILE_VERIFY_ON_FAILURE))
181 return r;
182
183 f = safe_fclose(f);
184
185 /* OK, the operation failed, but let's see if the right
186 * contents in place already. If so, eat up the error. */
187
188 q = verify_file(fn, line, !(flags & WRITE_STRING_FILE_AVOID_NEWLINE));
189 if (q <= 0)
190 return r;
191
192 return 0;
193 }
194
195 int write_string_filef(
196 const char *fn,
197 WriteStringFileFlags flags,
198 const char *format, ...) {
199
200 _cleanup_free_ char *p = NULL;
201 va_list ap;
202 int r;
203
204 va_start(ap, format);
205 r = vasprintf(&p, format, ap);
206 va_end(ap);
207
208 if (r < 0)
209 return -ENOMEM;
210
211 return write_string_file(fn, p, flags);
212 }
213
214 int read_one_line_file(const char *fn, char **line) {
215 _cleanup_fclose_ FILE *f = NULL;
216
217 assert(fn);
218 assert(line);
219
220 f = fopen(fn, "re");
221 if (!f)
222 return -errno;
223
224 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
225
226 return read_line(f, LONG_LINE_MAX, line);
227 }
228
229 int verify_file(const char *fn, const char *blob, bool accept_extra_nl) {
230 _cleanup_fclose_ FILE *f = NULL;
231 _cleanup_free_ char *buf = NULL;
232 size_t l, k;
233
234 assert(fn);
235 assert(blob);
236
237 l = strlen(blob);
238
239 if (accept_extra_nl && endswith(blob, "\n"))
240 accept_extra_nl = false;
241
242 buf = malloc(l + accept_extra_nl + 1);
243 if (!buf)
244 return -ENOMEM;
245
246 f = fopen(fn, "re");
247 if (!f)
248 return -errno;
249
250 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
251
252 /* We try to read one byte more than we need, so that we know whether we hit eof */
253 errno = 0;
254 k = fread(buf, 1, l + accept_extra_nl + 1, f);
255 if (ferror(f))
256 return errno > 0 ? -errno : -EIO;
257
258 if (k != l && k != l + accept_extra_nl)
259 return 0;
260 if (memcmp(buf, blob, l) != 0)
261 return 0;
262 if (k > l && buf[l] != '\n')
263 return 0;
264
265 return 1;
266 }
267
268 int read_full_stream_full(
269 FILE *f,
270 const char *filename,
271 ReadFullFileFlags flags,
272 char **ret_contents,
273 size_t *ret_size) {
274
275 _cleanup_free_ char *buf = NULL;
276 struct stat st;
277 size_t n, n_next, l;
278 int fd, r;
279
280 assert(f);
281 assert(ret_contents);
282 assert(!(flags & READ_FULL_FILE_UNBASE64) || ret_size);
283
284 n_next = LINE_MAX; /* Start size */
285
286 fd = fileno(f);
287 if (fd >= 0) { /* If the FILE* object is backed by an fd (as opposed to memory or such, see fmemopen(), let's
288 * optimize our buffering) */
289
290 if (fstat(fd, &st) < 0)
291 return -errno;
292
293 if (S_ISREG(st.st_mode)) {
294
295 /* Safety check */
296 if (st.st_size > READ_FULL_BYTES_MAX)
297 return -E2BIG;
298
299 /* Start with the right file size, but be prepared for files from /proc which generally report a file
300 * size of 0. Note that we increase the size to read here by one, so that the first read attempt
301 * already makes us notice the EOF. */
302 if (st.st_size > 0)
303 n_next = st.st_size + 1;
304
305 if (flags & READ_FULL_FILE_SECURE)
306 (void) warn_file_is_world_accessible(filename, &st, NULL, 0);
307 }
308 }
309
310 n = l = 0;
311 for (;;) {
312 char *t;
313 size_t k;
314
315 if (flags & READ_FULL_FILE_SECURE) {
316 t = malloc(n_next + 1);
317 if (!t) {
318 r = -ENOMEM;
319 goto finalize;
320 }
321 memcpy_safe(t, buf, n);
322 explicit_bzero_safe(buf, n);
323 } else {
324 t = realloc(buf, n_next + 1);
325 if (!t)
326 return -ENOMEM;
327 }
328
329 buf = t;
330 n = n_next;
331
332 errno = 0;
333 k = fread(buf + l, 1, n - l, f);
334 if (k > 0)
335 l += k;
336
337 if (ferror(f)) {
338 r = errno > 0 ? -errno : -EIO;
339 goto finalize;
340 }
341
342 if (feof(f))
343 break;
344
345 /* We aren't expecting fread() to return a short read outside
346 * of (error && eof), assert buffer is full and enlarge buffer.
347 */
348 assert(l == n);
349
350 /* Safety check */
351 if (n >= READ_FULL_BYTES_MAX) {
352 r = -E2BIG;
353 goto finalize;
354 }
355
356 n_next = MIN(n * 2, READ_FULL_BYTES_MAX);
357 }
358
359 if (flags & READ_FULL_FILE_UNBASE64) {
360 buf[l++] = 0;
361 r = unbase64mem_full(buf, l, flags & READ_FULL_FILE_SECURE, (void **) ret_contents, ret_size);
362 goto finalize;
363 }
364
365 if (!ret_size) {
366 /* Safety check: if the caller doesn't want to know the size of what we just read it will rely on the
367 * trailing NUL byte. But if there's an embedded NUL byte, then we should refuse operation as otherwise
368 * there'd be ambiguity about what we just read. */
369
370 if (memchr(buf, 0, l)) {
371 r = -EBADMSG;
372 goto finalize;
373 }
374 }
375
376 buf[l] = 0;
377 *ret_contents = TAKE_PTR(buf);
378
379 if (ret_size)
380 *ret_size = l;
381
382 return 0;
383
384 finalize:
385 if (flags & READ_FULL_FILE_SECURE)
386 explicit_bzero_safe(buf, n);
387
388 return r;
389 }
390
391 int read_full_file_full(const char *filename, ReadFullFileFlags flags, char **contents, size_t *size) {
392 _cleanup_fclose_ FILE *f = NULL;
393
394 assert(filename);
395 assert(contents);
396
397 f = fopen(filename, "re");
398 if (!f)
399 return -errno;
400
401 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
402
403 return read_full_stream_full(f, filename, flags, contents, size);
404 }
405
406 int executable_is_script(const char *path, char **interpreter) {
407 _cleanup_free_ char *line = NULL;
408 size_t len;
409 char *ans;
410 int r;
411
412 assert(path);
413
414 r = read_one_line_file(path, &line);
415 if (r == -ENOBUFS) /* First line overly long? if so, then it's not a script */
416 return 0;
417 if (r < 0)
418 return r;
419
420 if (!startswith(line, "#!"))
421 return 0;
422
423 ans = strstrip(line + 2);
424 len = strcspn(ans, " \t");
425
426 if (len == 0)
427 return 0;
428
429 ans = strndup(ans, len);
430 if (!ans)
431 return -ENOMEM;
432
433 *interpreter = ans;
434 return 1;
435 }
436
437 /**
438 * Retrieve one field from a file like /proc/self/status. pattern
439 * should not include whitespace or the delimiter (':'). pattern matches only
440 * the beginning of a line. Whitespace before ':' is skipped. Whitespace and
441 * zeros after the ':' will be skipped. field must be freed afterwards.
442 * terminator specifies the terminating characters of the field value (not
443 * included in the value).
444 */
445 int get_proc_field(const char *filename, const char *pattern, const char *terminator, char **field) {
446 _cleanup_free_ char *status = NULL;
447 char *t, *f;
448 size_t len;
449 int r;
450
451 assert(terminator);
452 assert(filename);
453 assert(pattern);
454 assert(field);
455
456 r = read_full_file(filename, &status, NULL);
457 if (r < 0)
458 return r;
459
460 t = status;
461
462 do {
463 bool pattern_ok;
464
465 do {
466 t = strstr(t, pattern);
467 if (!t)
468 return -ENOENT;
469
470 /* Check that pattern occurs in beginning of line. */
471 pattern_ok = (t == status || t[-1] == '\n');
472
473 t += strlen(pattern);
474
475 } while (!pattern_ok);
476
477 t += strspn(t, " \t");
478 if (!*t)
479 return -ENOENT;
480
481 } while (*t != ':');
482
483 t++;
484
485 if (*t) {
486 t += strspn(t, " \t");
487
488 /* Also skip zeros, because when this is used for
489 * capabilities, we don't want the zeros. This way the
490 * same capability set always maps to the same string,
491 * irrespective of the total capability set size. For
492 * other numbers it shouldn't matter. */
493 t += strspn(t, "0");
494 /* Back off one char if there's nothing but whitespace
495 and zeros */
496 if (!*t || isspace(*t))
497 t--;
498 }
499
500 len = strcspn(t, terminator);
501
502 f = strndup(t, len);
503 if (!f)
504 return -ENOMEM;
505
506 *field = f;
507 return 0;
508 }
509
510 DIR *xopendirat(int fd, const char *name, int flags) {
511 int nfd;
512 DIR *d;
513
514 assert(!(flags & O_CREAT));
515
516 nfd = openat(fd, name, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|flags, 0);
517 if (nfd < 0)
518 return NULL;
519
520 d = fdopendir(nfd);
521 if (!d) {
522 safe_close(nfd);
523 return NULL;
524 }
525
526 return d;
527 }
528
529 static int search_and_fopen_internal(const char *path, const char *mode, const char *root, char **search, FILE **_f) {
530 char **i;
531
532 assert(path);
533 assert(mode);
534 assert(_f);
535
536 if (!path_strv_resolve_uniq(search, root))
537 return -ENOMEM;
538
539 STRV_FOREACH(i, search) {
540 _cleanup_free_ char *p = NULL;
541 FILE *f;
542
543 if (root)
544 p = strjoin(root, *i, "/", path);
545 else
546 p = strjoin(*i, "/", path);
547 if (!p)
548 return -ENOMEM;
549
550 f = fopen(p, mode);
551 if (f) {
552 *_f = f;
553 return 0;
554 }
555
556 if (errno != ENOENT)
557 return -errno;
558 }
559
560 return -ENOENT;
561 }
562
563 int search_and_fopen(const char *path, const char *mode, const char *root, const char **search, FILE **_f) {
564 _cleanup_strv_free_ char **copy = NULL;
565
566 assert(path);
567 assert(mode);
568 assert(_f);
569
570 if (path_is_absolute(path)) {
571 FILE *f;
572
573 f = fopen(path, mode);
574 if (f) {
575 *_f = f;
576 return 0;
577 }
578
579 return -errno;
580 }
581
582 copy = strv_copy((char**) search);
583 if (!copy)
584 return -ENOMEM;
585
586 return search_and_fopen_internal(path, mode, root, copy, _f);
587 }
588
589 int search_and_fopen_nulstr(const char *path, const char *mode, const char *root, const char *search, FILE **_f) {
590 _cleanup_strv_free_ char **s = NULL;
591
592 if (path_is_absolute(path)) {
593 FILE *f;
594
595 f = fopen(path, mode);
596 if (f) {
597 *_f = f;
598 return 0;
599 }
600
601 return -errno;
602 }
603
604 s = strv_split_nulstr(search);
605 if (!s)
606 return -ENOMEM;
607
608 return search_and_fopen_internal(path, mode, root, s, _f);
609 }
610
611 int fflush_and_check(FILE *f) {
612 assert(f);
613
614 errno = 0;
615 fflush(f);
616
617 if (ferror(f))
618 return errno > 0 ? -errno : -EIO;
619
620 return 0;
621 }
622
623 int fflush_sync_and_check(FILE *f) {
624 int r;
625
626 assert(f);
627
628 r = fflush_and_check(f);
629 if (r < 0)
630 return r;
631
632 if (fsync(fileno(f)) < 0)
633 return -errno;
634
635 r = fsync_directory_of_file(fileno(f));
636 if (r < 0)
637 return r;
638
639 return 0;
640 }
641
642 int write_timestamp_file_atomic(const char *fn, usec_t n) {
643 char ln[DECIMAL_STR_MAX(n)+2];
644
645 /* Creates a "timestamp" file, that contains nothing but a
646 * usec_t timestamp, formatted in ASCII. */
647
648 if (n <= 0 || n >= USEC_INFINITY)
649 return -ERANGE;
650
651 xsprintf(ln, USEC_FMT "\n", n);
652
653 return write_string_file(fn, ln, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC);
654 }
655
656 int read_timestamp_file(const char *fn, usec_t *ret) {
657 _cleanup_free_ char *ln = NULL;
658 uint64_t t;
659 int r;
660
661 r = read_one_line_file(fn, &ln);
662 if (r < 0)
663 return r;
664
665 r = safe_atou64(ln, &t);
666 if (r < 0)
667 return r;
668
669 if (t <= 0 || t >= (uint64_t) USEC_INFINITY)
670 return -ERANGE;
671
672 *ret = (usec_t) t;
673 return 0;
674 }
675
676 int fputs_with_space(FILE *f, const char *s, const char *separator, bool *space) {
677 int r;
678
679 assert(s);
680
681 /* Outputs the specified string with fputs(), but optionally prefixes it with a separator. The *space parameter
682 * when specified shall initially point to a boolean variable initialized to false. It is set to true after the
683 * first invocation. This call is supposed to be use in loops, where a separator shall be inserted between each
684 * element, but not before the first one. */
685
686 if (!f)
687 f = stdout;
688
689 if (space) {
690 if (!separator)
691 separator = " ";
692
693 if (*space) {
694 r = fputs(separator, f);
695 if (r < 0)
696 return r;
697 }
698
699 *space = true;
700 }
701
702 return fputs(s, f);
703 }
704
705 /* A bitmask of the EOL markers we know */
706 typedef enum EndOfLineMarker {
707 EOL_NONE = 0,
708 EOL_ZERO = 1 << 0, /* \0 (aka NUL) */
709 EOL_TEN = 1 << 1, /* \n (aka NL, aka LF) */
710 EOL_THIRTEEN = 1 << 2, /* \r (aka CR) */
711 } EndOfLineMarker;
712
713 static EndOfLineMarker categorize_eol(char c, ReadLineFlags flags) {
714
715 if (!IN_SET(flags, READ_LINE_ONLY_NUL)) {
716 if (c == '\n')
717 return EOL_TEN;
718 if (c == '\r')
719 return EOL_THIRTEEN;
720 }
721
722 if (c == '\0')
723 return EOL_ZERO;
724
725 return EOL_NONE;
726 }
727
728 DEFINE_TRIVIAL_CLEANUP_FUNC(FILE*, funlockfile);
729
730 int read_line_full(FILE *f, size_t limit, ReadLineFlags flags, char **ret) {
731 size_t n = 0, allocated = 0, count = 0;
732 _cleanup_free_ char *buffer = NULL;
733 int r;
734
735 assert(f);
736
737 /* Something like a bounded version of getline().
738 *
739 * Considers EOF, \n, \r and \0 end of line delimiters (or combinations of these), and does not include these
740 * delimiters in the string returned. Specifically, recognizes the following combinations of markers as line
741 * endings:
742 *
743 * • \n (UNIX)
744 * • \r (old MacOS)
745 * • \0 (C strings)
746 * • \n\0
747 * • \r\0
748 * • \r\n (Windows)
749 * • \n\r
750 * • \r\n\0
751 * • \n\r\0
752 *
753 * Returns the number of bytes read from the files (i.e. including delimiters — this hence usually differs from
754 * the number of characters in the returned string). When EOF is hit, 0 is returned.
755 *
756 * The input parameter limit is the maximum numbers of characters in the returned string, i.e. excluding
757 * delimiters. If the limit is hit we fail and return -ENOBUFS.
758 *
759 * If a line shall be skipped ret may be initialized as NULL. */
760
761 if (ret) {
762 if (!GREEDY_REALLOC(buffer, allocated, 1))
763 return -ENOMEM;
764 }
765
766 {
767 _unused_ _cleanup_(funlockfilep) FILE *flocked = f;
768 EndOfLineMarker previous_eol = EOL_NONE;
769 flockfile(f);
770
771 for (;;) {
772 EndOfLineMarker eol;
773 char c;
774
775 if (n >= limit)
776 return -ENOBUFS;
777
778 if (count >= INT_MAX) /* We couldn't return the counter anymore as "int", hence refuse this */
779 return -ENOBUFS;
780
781 r = safe_fgetc(f, &c);
782 if (r < 0)
783 return r;
784 if (r == 0) /* EOF is definitely EOL */
785 break;
786
787 eol = categorize_eol(c, flags);
788
789 if (FLAGS_SET(previous_eol, EOL_ZERO) ||
790 (eol == EOL_NONE && previous_eol != EOL_NONE) ||
791 (eol != EOL_NONE && (previous_eol & eol) != 0)) {
792 /* Previous char was a NUL? This is not an EOL, but the previous char was? This type of
793 * EOL marker has been seen right before? In either of these three cases we are
794 * done. But first, let's put this character back in the queue. (Note that we have to
795 * cast this to (unsigned char) here as ungetc() expects a positive 'int', and if we
796 * are on an architecture where 'char' equals 'signed char' we need to ensure we don't
797 * pass a negative value here. That said, to complicate things further ungetc() is
798 * actually happy with most negative characters and implicitly casts them back to
799 * positive ones as needed, except for \xff (aka -1, aka EOF), which it refuses. What a
800 * godawful API!) */
801 assert_se(ungetc((unsigned char) c, f) != EOF);
802 break;
803 }
804
805 count++;
806
807 if (eol != EOL_NONE) {
808 previous_eol |= eol;
809 continue;
810 }
811
812 if (ret) {
813 if (!GREEDY_REALLOC(buffer, allocated, n + 2))
814 return -ENOMEM;
815
816 buffer[n] = c;
817 }
818
819 n++;
820 }
821 }
822
823 if (ret) {
824 buffer[n] = 0;
825
826 *ret = TAKE_PTR(buffer);
827 }
828
829 return (int) count;
830 }
831
832 int safe_fgetc(FILE *f, char *ret) {
833 int k;
834
835 assert(f);
836
837 /* A safer version of plain fgetc(): let's propagate the error that happened while reading as such, and
838 * separate the EOF condition from the byte read, to avoid those confusion signed/unsigned issues fgetc()
839 * has. */
840
841 errno = 0;
842 k = fgetc(f);
843 if (k == EOF) {
844 if (ferror(f))
845 return errno > 0 ? -errno : -EIO;
846
847 if (ret)
848 *ret = 0;
849
850 return 0;
851 }
852
853 if (ret)
854 *ret = k;
855
856 return 1;
857 }
858
859 int warn_file_is_world_accessible(const char *filename, struct stat *st, const char *unit, unsigned line) {
860 struct stat _st;
861
862 if (!filename)
863 return 0;
864
865 if (!st) {
866 if (stat(filename, &_st) < 0)
867 return -errno;
868 st = &_st;
869 }
870
871 if ((st->st_mode & S_IRWXO) == 0)
872 return 0;
873
874 if (unit)
875 log_syntax(unit, LOG_WARNING, filename, line, 0,
876 "%s has %04o mode that is too permissive, please adjust the access mode.",
877 filename, st->st_mode & 07777);
878 else
879 log_warning("%s has %04o mode that is too permissive, please adjust the access mode.",
880 filename, st->st_mode & 07777);
881 return 0;
882 }