Commit | Line | Data |
---|---|---|
6a550a97 WJ |
1 | #if !defined(lint) && !defined(LINT) |
2 | static char rcsid[] = "$Header: misc.c,v 2.1 90/07/18 00:24:33 vixie Exp $"; | |
3 | #endif | |
4 | ||
5 | /* vix 26jan87 [RCS has the rest of the log] | |
6 | * vix 15jan87 [added TIOCNOTTY, thanks csg@pyramid] | |
7 | * vix 30dec86 [written] | |
8 | */ | |
9 | ||
10 | /* Copyright 1988,1990 by Paul Vixie | |
11 | * All rights reserved | |
12 | * | |
13 | * Distribute freely, except: don't remove my name from the source or | |
14 | * documentation (don't take credit for my work), mark your changes (don't | |
15 | * get me blamed for your possible bugs), don't alter or remove this | |
16 | * notice. May be sold if buildable source is provided to buyer. No | |
17 | * warrantee of any kind, express or implied, is included with this | |
18 | * software; use at your own risk, responsibility for damages (if any) to | |
19 | * anyone resulting from the use of this software rests entirely with the | |
20 | * user. | |
21 | * | |
22 | * Send bug reports, bug fixes, enhancements, requests, flames, etc., and | |
23 | * I'll try to keep a version up to date. I can be reached as follows: | |
24 | * Paul Vixie, 329 Noe Street, San Francisco, CA, 94114, (415) 864-7013, | |
25 | * paul@vixie.sf.ca.us || {hoptoad,pacbell,decwrl,crash}!vixie!paul | |
26 | */ | |
27 | ||
28 | ||
29 | #include "cron.h" | |
30 | #include <sys/time.h> | |
31 | #include <sys/resource.h> | |
32 | #include <sys/ioctl.h> | |
33 | #include <sys/file.h> | |
34 | #include <errno.h> | |
35 | #if defined(ATT) | |
36 | # include <fcntl.h> | |
37 | #endif | |
38 | ||
39 | ||
40 | void log_it(), be_different(), acquire_daemonlock(); | |
41 | ||
42 | ||
43 | char * | |
44 | savestr(str) | |
45 | char *str; | |
46 | { | |
47 | extern int strlen(); | |
48 | extern char *malloc(), *strcpy(); | |
49 | /**/ char *temp; | |
50 | ||
51 | temp = malloc((unsigned) (strlen(str) + 1)); | |
52 | (void) strcpy(temp, str); | |
53 | return temp; | |
54 | } | |
55 | ||
56 | ||
57 | int | |
58 | nocase_strcmp(left, right) | |
59 | char *left; | |
60 | char *right; | |
61 | { | |
62 | while (*left && (MkLower(*left) == MkLower(*right))) | |
63 | { | |
64 | left++; | |
65 | right++; | |
66 | } | |
67 | return MkLower(*left) - MkLower(*right); | |
68 | } | |
69 | ||
70 | ||
71 | int | |
72 | strcmp_until(left, right, until) | |
73 | char *left; | |
74 | char *right; | |
75 | char until; | |
76 | { | |
77 | register int diff; | |
78 | ||
79 | Debug(DPARS|DEXT, ("strcmp_until(%s,%s,%c) ... ", left, right, until)) | |
80 | ||
81 | while (*left && *left != until && *left == *right) | |
82 | { | |
83 | left++; | |
84 | right++; | |
85 | } | |
86 | ||
87 | if ( (*left=='\0' || *left == until) | |
88 | && (*right=='\0' || *right == until) | |
89 | ) | |
90 | diff = 0; | |
91 | else | |
92 | diff = *left - *right; | |
93 | ||
94 | Debug(DPARS|DEXT, ("%d\n", diff)) | |
95 | ||
96 | return diff; | |
97 | } | |
98 | ||
99 | ||
100 | /* strdtb(s) - delete trailing blanks in string 's' and return new length | |
101 | */ | |
102 | int | |
103 | strdtb(s) | |
104 | register char *s; | |
105 | { | |
106 | register char *x = s; | |
107 | ||
108 | /* scan forward to the null | |
109 | */ | |
110 | while (*x) | |
111 | x++; | |
112 | ||
113 | /* scan backward to either the first character before the string, | |
114 | * or the last non-blank in the string, whichever comes first. | |
115 | */ | |
116 | do {x--;} | |
117 | while (x >= s && isspace(*x)); | |
118 | ||
119 | /* one character beyond where we stopped above is where the null | |
120 | * goes. | |
121 | */ | |
122 | *++x = '\0'; | |
123 | ||
124 | /* the difference between the position of the null character and | |
125 | * the position of the first character of the string is the length. | |
126 | */ | |
127 | return x - s; | |
128 | } | |
129 | ||
130 | ||
131 | int | |
132 | set_debug_flags(flags) | |
133 | char *flags; | |
134 | { | |
135 | /* debug flags are of the form flag[,flag ...] | |
136 | * | |
137 | * if an error occurs, print a message to stdout and return FALSE. | |
138 | * otherwise return TRUE after setting ERROR_FLAGS. | |
139 | */ | |
140 | ||
141 | #if !DEBUGGING | |
142 | ||
143 | printf("this program was compiled without debugging enabled\n"); | |
144 | return FALSE; | |
145 | ||
146 | #else /* DEBUGGING */ | |
147 | ||
148 | char *pc = flags; | |
149 | ||
150 | DebugFlags = 0; | |
151 | ||
152 | while (*pc) | |
153 | { | |
154 | char **test; | |
155 | int mask; | |
156 | ||
157 | /* try to find debug flag name in our list. | |
158 | */ | |
159 | for ( test = DebugFlagNames, mask = 1; | |
160 | *test && strcmp_until(*test, pc, ','); | |
161 | test++, mask <<= 1 | |
162 | ) | |
163 | ; | |
164 | ||
165 | if (!*test) | |
166 | { | |
167 | fprintf(stderr, | |
168 | "unrecognized debug flag <%s> <%s>\n", | |
169 | flags, pc); | |
170 | return FALSE; | |
171 | } | |
172 | ||
173 | DebugFlags |= mask; | |
174 | ||
175 | /* skip to the next flag | |
176 | */ | |
177 | while (*pc && *pc != ',') | |
178 | pc++; | |
179 | if (*pc == ',') | |
180 | pc++; | |
181 | } | |
182 | ||
183 | if (DebugFlags) | |
184 | { | |
185 | int flag; | |
186 | ||
187 | fprintf(stderr, "debug flags enabled:"); | |
188 | ||
189 | for (flag = 0; DebugFlagNames[flag]; flag++) | |
190 | if (DebugFlags & (1 << flag)) | |
191 | fprintf(stderr, " %s", DebugFlagNames[flag]); | |
192 | fprintf(stderr, "\n"); | |
193 | } | |
194 | ||
195 | return TRUE; | |
196 | ||
197 | #endif /* DEBUGGING */ | |
198 | } | |
199 | ||
200 | ||
201 | #if defined(BSD) | |
202 | void | |
203 | set_cron_uid() | |
204 | { | |
205 | int seteuid(); | |
206 | ||
207 | if (seteuid(ROOT_UID) < OK) | |
208 | { | |
209 | perror("seteuid"); | |
210 | exit(ERROR_EXIT); | |
211 | } | |
212 | } | |
213 | #endif | |
214 | ||
215 | #if defined(ATT) | |
216 | void | |
217 | set_cron_uid() | |
218 | { | |
219 | int setuid(); | |
220 | ||
221 | if (setuid(ROOT_UID) < OK) | |
222 | { | |
223 | perror("setuid"); | |
224 | exit(ERROR_EXIT); | |
225 | } | |
226 | } | |
227 | #endif | |
228 | ||
229 | void | |
230 | set_cron_cwd() | |
231 | { | |
232 | extern int errno; | |
233 | struct stat sb; | |
234 | ||
235 | /* first check for CRONDIR ("/var/cron" or some such) | |
236 | */ | |
237 | if (stat(CRONDIR, &sb) < OK && errno == ENOENT) { | |
238 | perror(CRONDIR); | |
239 | if (OK == mkdir(CRONDIR, 0700)) { | |
240 | fprintf(stderr, "%s: created\n", CRONDIR); | |
241 | stat(CRONDIR, &sb); | |
242 | } else { | |
243 | fprintf(stderr, "%s: ", CRONDIR); | |
244 | perror("mkdir"); | |
245 | exit(ERROR_EXIT); | |
246 | } | |
247 | } | |
248 | if (!(sb.st_mode & S_IFDIR)) { | |
249 | fprintf(stderr, "'%s' is not a directory, bailing out.\n", | |
250 | CRONDIR); | |
251 | exit(ERROR_EXIT); | |
252 | } | |
253 | if (chdir(CRONDIR) < OK) { | |
254 | fprintf(stderr, "cannot chdir(%s), bailing out.\n", CRONDIR); | |
255 | perror(CRONDIR); | |
256 | exit(ERROR_EXIT); | |
257 | } | |
258 | ||
259 | /* CRONDIR okay (now==CWD), now look at SPOOL_DIR ("tabs" or some such) | |
260 | */ | |
261 | if (stat(SPOOL_DIR, &sb) < OK && errno == ENOENT) { | |
262 | perror(SPOOL_DIR); | |
263 | if (OK == mkdir(SPOOL_DIR, 0700)) { | |
264 | fprintf(stderr, "%s: created\n", SPOOL_DIR); | |
265 | stat(SPOOL_DIR, &sb); | |
266 | } else { | |
267 | fprintf(stderr, "%s: ", SPOOL_DIR); | |
268 | perror("mkdir"); | |
269 | exit(ERROR_EXIT); | |
270 | } | |
271 | } | |
272 | if (!(sb.st_mode & S_IFDIR)) { | |
273 | fprintf(stderr, "'%s' is not a directory, bailing out.\n", | |
274 | SPOOL_DIR); | |
275 | exit(ERROR_EXIT); | |
276 | } | |
277 | } | |
278 | ||
279 | ||
280 | #if defined(BSD) | |
281 | void | |
282 | be_different() | |
283 | { | |
284 | /* release the control terminal: | |
285 | * get new pgrp (name after our PID) | |
286 | * do an IOCTL to void tty association | |
287 | */ | |
288 | ||
289 | extern int getpid(), setpgrp(), open(), ioctl(), close(); | |
290 | auto int fd; | |
291 | ||
292 | (void) setpgrp(0, getpid()); | |
293 | ||
294 | if ((fd = open("/dev/tty", 2)) >= 0) | |
295 | { | |
296 | (void) ioctl(fd, TIOCNOTTY, (char*)0); | |
297 | (void) close(fd); | |
298 | } | |
299 | } | |
300 | #endif /*BSD*/ | |
301 | ||
302 | #if defined(ATT) | |
303 | void | |
304 | be_different() | |
305 | { | |
306 | /* not being a system V wiz, I don't know if this is what you have | |
307 | * to do to release your control terminal. what I want to accomplish | |
308 | * is to keep this process from getting any signals from the tty. | |
309 | * | |
310 | * some system V person should let me know if this works... (vixie) | |
311 | */ | |
312 | int setpgrp(), close(), open(); | |
313 | ||
314 | (void) setpgrp(); | |
315 | ||
316 | (void) close(STDIN); (void) open("/dev/null", 0); | |
317 | (void) close(STDOUT); (void) open("/dev/null", 1); | |
318 | (void) close(STDERR); (void) open("/dev/null", 2); | |
319 | } | |
320 | #endif /*ATT*/ | |
321 | ||
322 | ||
323 | /* acquire_daemonlock() - write our PID into /etc/crond.pid, unless | |
324 | * another daemon is already running, which we detect here. | |
325 | */ | |
326 | void | |
327 | acquire_daemonlock() | |
328 | { | |
329 | int fd = open(PIDFILE, O_RDWR|O_CREAT, 0644); | |
330 | FILE *fp = fdopen(fd, "r+"); | |
331 | int pid = getpid(), otherpid; | |
332 | char buf[MAX_TEMPSTR]; | |
333 | ||
334 | if (fd < 0 || fp == NULL) { | |
335 | sprintf(buf, "can't open or create %s, errno %d", PIDFILE, errno); | |
336 | log_it("CROND", pid, "DEATH", buf); | |
337 | exit(ERROR_EXIT); | |
338 | } | |
339 | ||
340 | if (flock(fd, LOCK_EX|LOCK_NB) < OK) { | |
341 | int save_errno = errno; | |
342 | ||
343 | fscanf(fp, "%d", &otherpid); | |
344 | sprintf(buf, "can't lock %s, otherpid may be %d, errno %d", | |
345 | PIDFILE, otherpid, save_errno); | |
346 | log_it("CROND", pid, "DEATH", buf); | |
347 | exit(ERROR_EXIT); | |
348 | } | |
349 | ||
350 | rewind(fp); | |
351 | fprintf(fp, "%d\n", pid); | |
352 | fflush(fp); | |
353 | ftruncate(fd, ftell(fp)); | |
354 | ||
355 | /* abandon fd and fp even though the file is open. we need to | |
356 | * keep it open and locked, but we don't need the handles elsewhere. | |
357 | */ | |
358 | } | |
359 | ||
360 | /* get_char(file) : like getc() but increment LineNumber on newlines | |
361 | */ | |
362 | int | |
363 | get_char(file) | |
364 | FILE *file; | |
365 | { | |
366 | int ch; | |
367 | ||
368 | ch = getc(file); | |
369 | if (ch == '\n') | |
370 | Set_LineNum(LineNumber + 1) | |
371 | return ch; | |
372 | } | |
373 | ||
374 | ||
375 | /* unget_char(ch, file) : like ungetc but do LineNumber processing | |
376 | */ | |
377 | void | |
378 | unget_char(ch, file) | |
379 | int ch; | |
380 | FILE *file; | |
381 | { | |
382 | ungetc(ch, file); | |
383 | if (ch == '\n') | |
384 | Set_LineNum(LineNumber - 1) | |
385 | } | |
386 | ||
387 | ||
388 | /* get_string(str, max, file, termstr) : like fgets() but | |
389 | * (1) has terminator string which should include \n | |
390 | * (2) will always leave room for the null | |
391 | * (3) uses get_char() so LineNumber will be accurate | |
392 | * (4) returns EOF or terminating character, whichever | |
393 | */ | |
394 | int | |
395 | get_string(string, size, file, terms) | |
396 | char *string; | |
397 | int size; | |
398 | FILE *file; | |
399 | char *terms; | |
400 | { | |
401 | int ch; | |
402 | char *index(); | |
403 | ||
404 | while (EOF != (ch = get_char(file)) && !index(terms, ch)) | |
405 | if (size > 1) | |
406 | { | |
407 | *string++ = (char) ch; | |
408 | size--; | |
409 | } | |
410 | ||
411 | if (size > 0) | |
412 | *string = '\0'; | |
413 | ||
414 | return ch; | |
415 | } | |
416 | ||
417 | ||
418 | /* skip_comments(file) : read past comment (if any) | |
419 | */ | |
420 | void | |
421 | skip_comments(file) | |
422 | FILE *file; | |
423 | { | |
424 | int ch; | |
425 | ||
426 | while (EOF != (ch = get_char(file))) | |
427 | { | |
428 | /* ch is now the first character of a line. | |
429 | */ | |
430 | ||
431 | while (ch == ' ' || ch == '\t') | |
432 | ch = get_char(file); | |
433 | ||
434 | if (ch == EOF) | |
435 | break; | |
436 | ||
437 | /* ch is now the first non-blank character of a line. | |
438 | */ | |
439 | ||
440 | if (ch != '\n' && ch != '#') | |
441 | break; | |
442 | ||
443 | /* ch must be a newline or comment as first non-blank | |
444 | * character on a line. | |
445 | */ | |
446 | ||
447 | while (ch != '\n' && ch != EOF) | |
448 | ch = get_char(file); | |
449 | ||
450 | /* ch is now the newline of a line which we're going to | |
451 | * ignore. | |
452 | */ | |
453 | } | |
454 | unget_char(ch, file); | |
455 | } | |
456 | ||
457 | /* int in_file(char *string, FILE *file) | |
458 | * return TRUE if one of the lines in file matches string exactly, | |
459 | * FALSE otherwise. | |
460 | */ | |
461 | int | |
462 | in_file(string, file) | |
463 | char *string; | |
464 | FILE *file; | |
465 | { | |
466 | char line[MAX_TEMPSTR]; | |
467 | ||
468 | /* let's be persnickety today. | |
469 | */ | |
470 | if (!file) { | |
471 | if (!string) | |
472 | string = "0x0"; | |
473 | fprintf(stderr, | |
474 | "in_file(\"%s\", 0x%x): called with NULL file--botch", | |
475 | string, file); | |
476 | exit(ERROR_EXIT); | |
477 | } | |
478 | ||
479 | rewind(file); | |
480 | while (fgets(line, MAX_TEMPSTR, file)) { | |
481 | if (line[0] != '\0') | |
482 | line[strlen(line)-1] = '\0'; | |
483 | if (0 == strcmp(line, string)) | |
484 | return TRUE; | |
485 | } | |
486 | return FALSE; | |
487 | } | |
488 | ||
489 | ||
490 | /* int allowed(char *username) | |
491 | * returns TRUE if (ALLOW_FILE exists and user is listed) | |
492 | * or (DENY_FILE exists and user is NOT listed) | |
493 | * or (neither file exists but user=="root" so it's okay) | |
494 | */ | |
495 | int | |
496 | allowed(username) | |
497 | char *username; | |
498 | { | |
499 | static int init = FALSE; | |
500 | static FILE *allow, *deny; | |
501 | ||
502 | if (!init) { | |
503 | init = TRUE; | |
504 | #if defined(ALLOW_FILE) && defined(DENY_FILE) | |
505 | allow = fopen(ALLOW_FILE, "r"); | |
506 | deny = fopen(DENY_FILE, "r"); | |
507 | Debug(DMISC, ("allow/deny enabled, %d/%d\n", !!allow, !!deny)) | |
508 | #else | |
509 | allow = NULL; | |
510 | deny = NULL; | |
511 | #endif | |
512 | } | |
513 | ||
514 | if (allow) | |
515 | return (in_file(username, allow)); | |
516 | if (deny) | |
517 | return (!in_file(username, deny)); | |
518 | ||
519 | #if defined(ALLOW_ONLY_ROOT) | |
520 | return (strcmp(username, ROOT_USER) == 0); | |
521 | #else | |
522 | return TRUE; | |
523 | #endif | |
524 | } | |
525 | ||
526 | ||
527 | #if defined(LOG_FILE) || defined(SYSLOG) | |
528 | void | |
529 | log_it(username, pid, event, detail) | |
530 | char *username; | |
531 | int pid; | |
532 | char *event; | |
533 | char *detail; | |
534 | { | |
535 | #if defined(LOG_FILE) | |
536 | extern struct tm *localtime(); | |
537 | extern long time(); | |
538 | extern char *malloc(); | |
539 | auto char *msg; | |
540 | auto long now = time((long *) 0); | |
541 | register struct tm *t = localtime(&now); | |
542 | static int log_fd = -1; | |
543 | #endif /*LOG_FILE*/ | |
544 | ||
545 | #if defined(SYSLOG) | |
546 | static int syslog_open = 0; | |
547 | #endif | |
548 | ||
549 | ||
550 | #if defined(LOG_FILE) | |
551 | /* we assume that MAX_TEMPSTR will hold the date, time, &punctuation. | |
552 | */ | |
553 | msg = malloc(strlen(username) | |
554 | + strlen(event) | |
555 | + strlen(detail) | |
556 | + MAX_TEMPSTR); | |
557 | ||
558 | if (log_fd < OK) { | |
559 | log_fd = open(LOG_FILE, O_WRONLY|O_APPEND|O_CREAT, 0600); | |
560 | if (log_fd < OK) { | |
561 | fprintf(stderr, "%s: can't open log file\n", ProgramName); | |
562 | perror(LOG_FILE); | |
563 | } | |
564 | } | |
565 | ||
566 | /* we have to sprintf() it because fprintf() doesn't always write | |
567 | * everything out in one chunk and this has to be atomically appended | |
568 | * to the log file. | |
569 | */ | |
570 | sprintf(msg, "%s (%02d/%02d-%02d:%02d:%02d-%d) %s (%s)\n", | |
571 | username, | |
572 | t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, pid, | |
573 | event, detail); | |
574 | ||
575 | /* we have to run strlen() because sprintf() returns (char*) on BSD | |
576 | */ | |
577 | if (log_fd < OK || write(log_fd, msg, strlen(msg)) < OK) { | |
578 | fprintf(stderr, "%s: can't write to log file\n", ProgramName); | |
579 | if (log_fd >= OK) | |
580 | perror(LOG_FILE); | |
581 | write(STDERR, msg, strlen(msg)); | |
582 | } | |
583 | ||
584 | /* I suppose we could use alloca()... | |
585 | */ | |
586 | free(msg); | |
587 | #endif /*LOG_FILE*/ | |
588 | ||
589 | #if defined(SYSLOG) | |
590 | if (!syslog_open) { | |
591 | /* we don't use LOG_PID since the pid passed to us by | |
592 | * our client may not be our own. therefore we want to | |
593 | * print the pid ourselves. | |
594 | */ | |
595 | # ifdef LOG_CRON | |
596 | openlog(ProgramName, 0, LOG_CRON); | |
597 | # else | |
598 | # ifdef LOG_DAEMON | |
599 | openlog(ProgramName, 0, LOG_DAEMON); | |
600 | # else | |
601 | openlog(ProgramName, 0); | |
602 | # endif /*LOG_DAEMON*/ | |
603 | # endif /*LOG_CRON*/ | |
604 | syslog_open = TRUE; /* assume openlog success */ | |
605 | } | |
606 | ||
607 | syslog(LOG_INFO, "(%s %d) %s (%s)\n", | |
608 | username, pid, event, detail); | |
609 | ||
610 | #endif /*SYSLOG*/ | |
611 | ||
612 | if (DebugFlags) { | |
613 | fprintf(stderr, "log_it: (%s %d) %s (%s)", | |
614 | username, pid, event, detail); | |
615 | } | |
616 | } | |
617 | #endif /*LOG_FILE || SYSLOG */ | |
618 | ||
619 | ||
620 | /* two warnings: | |
621 | * (1) this routine is fairly slow | |
622 | * (2) it returns a pointer to static storage | |
623 | */ | |
624 | char * | |
625 | first_word(s, t) | |
626 | local char *s; /* string we want the first word of */ | |
627 | local char *t; /* terminators, implicitly including \0 */ | |
628 | { | |
629 | static char retbuf[2][MAX_TEMPSTR + 1]; /* sure wish I had GC */ | |
630 | static int retsel = 0; | |
631 | local char *rb, *rp; | |
632 | extern char *index(); | |
633 | ||
634 | /* select a return buffer */ | |
635 | retsel = 1-retsel; | |
636 | rb = &retbuf[retsel][0]; | |
637 | rp = rb; | |
638 | ||
639 | /* skip any leading terminators */ | |
640 | while (*s && (NULL != index(t, *s))) {s++;} | |
641 | ||
642 | /* copy until next terminator or full buffer */ | |
643 | while (*s && (NULL == index(t, *s)) && (rp < &rb[MAX_TEMPSTR])) { | |
644 | *rp++ = *s++; | |
645 | } | |
646 | ||
647 | /* finish the return-string and return it */ | |
648 | *rp = '\0'; | |
649 | return rb; | |
650 | } | |
651 | ||
652 | ||
653 | /* warning: | |
654 | * heavily ascii-dependent. | |
655 | */ | |
656 | ||
657 | void | |
658 | mkprint(dst, src, len) | |
659 | register char *dst; | |
660 | register unsigned char *src; | |
661 | register int len; | |
662 | { | |
663 | while (len-- > 0) | |
664 | { | |
665 | register unsigned char ch = *src++; | |
666 | ||
667 | if (ch < ' ') { /* control character */ | |
668 | *dst++ = '^'; | |
669 | *dst++ = ch + '@'; | |
670 | } else if (ch < 0177) { /* printable */ | |
671 | *dst++ = ch; | |
672 | } else if (ch == 0177) { /* delete/rubout */ | |
673 | *dst++ = '^'; | |
674 | *dst++ = '?'; | |
675 | } else { /* parity character */ | |
676 | sprintf(dst, "\\%03o", ch); | |
677 | dst += 4; | |
678 | } | |
679 | } | |
680 | *dst = NULL; | |
681 | } | |
682 | ||
683 | ||
684 | /* warning: | |
685 | * returns a pointer to malloc'd storage, you must call free yourself. | |
686 | */ | |
687 | ||
688 | char * | |
689 | mkprints(src, len) | |
690 | register unsigned char *src; | |
691 | register unsigned int len; | |
692 | { | |
693 | extern char *malloc(); | |
694 | register char *dst = malloc(len*4 + 1); | |
695 | ||
696 | mkprint(dst, src, len); | |
697 | ||
698 | return dst; | |
699 | } |