1 /*
2 * uxstore.c: Unix-specific implementation of the interface defined
3 * in storage.h.
4 */
5
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <assert.h>
10 #include <errno.h>
11 #include <ctype.h>
12 #include <limits.h>
13 #include <unistd.h>
14 #include <fcntl.h>
15 #include <dirent.h>
16 #include <sys/stat.h>
17 #include <sys/types.h>
18 #include <pwd.h>
19 #include "putty.h"
20 #include "storage.h"
21 #include "tree234.h"
22
23 #ifdef PATH_MAX
24 #define FNLEN PATH_MAX
25 #else
26 #define FNLEN 1024 /* XXX */
27 #endif
28
29 enum {
30 INDEX_DIR, INDEX_HOSTKEYS, INDEX_HOSTKEYS_TMP, INDEX_RANDSEED,
31 INDEX_SESSIONDIR, INDEX_SESSION,
32 };
33
34 static const char hex[16] = "0123456789ABCDEF";
35
make_session_filename(const char * in,strbuf * out)36 static void make_session_filename(const char *in, strbuf *out)
37 {
38 if (!in || !*in)
39 in = "Default Settings";
40
41 while (*in) {
42 /*
43 * There are remarkably few punctuation characters that
44 * aren't shell-special in some way or likely to be used as
45 * separators in some file format or another! Hence we use
46 * opt-in for safe characters rather than opt-out for
47 * specific unsafe ones...
48 */
49 if (*in!='+' && *in!='-' && *in!='.' && *in!='@' && *in!='_' &&
50 !(*in >= '0' && *in <= '9') &&
51 !(*in >= 'A' && *in <= 'Z') &&
52 !(*in >= 'a' && *in <= 'z')) {
53 put_byte(out, '%');
54 put_byte(out, hex[((unsigned char) *in) >> 4]);
55 put_byte(out, hex[((unsigned char) *in) & 15]);
56 } else
57 put_byte(out, *in);
58 in++;
59 }
60 }
61
decode_session_filename(const char * in,strbuf * out)62 static void decode_session_filename(const char *in, strbuf *out)
63 {
64 while (*in) {
65 if (*in == '%' && in[1] && in[2]) {
66 int i, j;
67
68 i = in[1] - '0';
69 i -= (i > 9 ? 7 : 0);
70 j = in[2] - '0';
71 j -= (j > 9 ? 7 : 0);
72
73 put_byte(out, (i << 4) + j);
74 in += 3;
75 } else {
76 put_byte(out, *in++);
77 }
78 }
79 }
80
make_filename(int index,const char * subname)81 static char *make_filename(int index, const char *subname)
82 {
83 char *env, *tmp, *ret;
84
85 /*
86 * Allow override of the PuTTY configuration location, and of
87 * specific subparts of it, by means of environment variables.
88 */
89 if (index == INDEX_DIR) {
90 struct passwd *pwd;
91 char *xdg_dir, *old_dir, *old_dir2, *old_dir3, *home, *pwd_home;
92
93 env = getenv("PUTTYDIR");
94 if (env)
95 return dupstr(env);
96
97 home = getenv("HOME");
98 pwd = getpwuid(getuid());
99 if (pwd && pwd->pw_dir) {
100 pwd_home = pwd->pw_dir;
101 } else {
102 pwd_home = NULL;
103 }
104
105 xdg_dir = NULL;
106 env = getenv("XDG_CONFIG_HOME");
107 if (env && *env) {
108 xdg_dir = dupprintf("%s/putty", env);
109 }
110 if (!xdg_dir) {
111 if (home) {
112 tmp = home;
113 } else if (pwd_home) {
114 tmp = pwd_home;
115 } else {
116 tmp = "";
117 }
118 xdg_dir = dupprintf("%s/.config/putty", tmp);
119 }
120 if (xdg_dir && access(xdg_dir, F_OK) == 0) {
121 return xdg_dir;
122 }
123
124 old_dir = old_dir2 = old_dir3 = NULL;
125 if (home) {
126 old_dir = dupprintf("%s/.putty", home);
127 }
128 if (pwd_home) {
129 old_dir2 = dupprintf("%s/.putty", pwd_home);
130 }
131 old_dir3 = dupstr("/.putty");
132
133 if (old_dir && access(old_dir, F_OK) == 0) {
134 ret = old_dir;
135 goto out;
136 }
137 if (old_dir2 && access(old_dir2, F_OK) == 0) {
138 ret = old_dir2;
139 goto out;
140 }
141 if (access(old_dir3, F_OK) == 0) {
142 ret = old_dir3;
143 goto out;
144 }
145 #ifdef XDG_DEFAULT
146 if (xdg_dir) {
147 ret = xdg_dir;
148 goto out;
149 }
150 #endif
151 ret = old_dir ? old_dir : (old_dir2 ? old_dir2 : old_dir3);
152
153 out:
154 if (ret != old_dir)
155 sfree(old_dir);
156 if (ret != old_dir2)
157 sfree(old_dir2);
158 if (ret != old_dir3)
159 sfree(old_dir3);
160 if (ret != xdg_dir)
161 sfree(xdg_dir);
162 return ret;
163 }
164 if (index == INDEX_SESSIONDIR) {
165 env = getenv("PUTTYSESSIONS");
166 if (env)
167 return dupstr(env);
168 tmp = make_filename(INDEX_DIR, NULL);
169 ret = dupprintf("%s/sessions", tmp);
170 sfree(tmp);
171 return ret;
172 }
173 if (index == INDEX_SESSION) {
174 strbuf *sb = strbuf_new();
175 tmp = make_filename(INDEX_SESSIONDIR, NULL);
176 strbuf_catf(sb, "%s/", tmp);
177 sfree(tmp);
178 make_session_filename(subname, sb);
179 return strbuf_to_str(sb);
180 }
181 if (index == INDEX_HOSTKEYS) {
182 env = getenv("PUTTYSSHHOSTKEYS");
183 if (env)
184 return dupstr(env);
185 tmp = make_filename(INDEX_DIR, NULL);
186 ret = dupprintf("%s/sshhostkeys", tmp);
187 sfree(tmp);
188 return ret;
189 }
190 if (index == INDEX_HOSTKEYS_TMP) {
191 tmp = make_filename(INDEX_HOSTKEYS, NULL);
192 ret = dupprintf("%s.tmp", tmp);
193 sfree(tmp);
194 return ret;
195 }
196 if (index == INDEX_RANDSEED) {
197 env = getenv("PUTTYRANDOMSEED");
198 if (env)
199 return dupstr(env);
200 tmp = make_filename(INDEX_DIR, NULL);
201 ret = dupprintf("%s/randomseed", tmp);
202 sfree(tmp);
203 return ret;
204 }
205 tmp = make_filename(INDEX_DIR, NULL);
206 ret = dupprintf("%s/ERROR", tmp);
207 sfree(tmp);
208 return ret;
209 }
210
211 struct settings_w {
212 FILE *fp;
213 };
214
open_settings_w(const char * sessionname,char ** errmsg)215 settings_w *open_settings_w(const char *sessionname, char **errmsg)
216 {
217 char *filename, *err;
218 FILE *fp;
219
220 *errmsg = NULL;
221
222 /*
223 * Start by making sure the .putty directory and its sessions
224 * subdir actually exist.
225 */
226 filename = make_filename(INDEX_DIR, NULL);
227 if ((err = make_dir_path(filename, 0700)) != NULL) {
228 *errmsg = dupprintf("Unable to save session: %s", err);
229 sfree(err);
230 sfree(filename);
231 return NULL;
232 }
233 sfree(filename);
234
235 filename = make_filename(INDEX_SESSIONDIR, NULL);
236 if ((err = make_dir_path(filename, 0700)) != NULL) {
237 *errmsg = dupprintf("Unable to save session: %s", err);
238 sfree(err);
239 sfree(filename);
240 return NULL;
241 }
242 sfree(filename);
243
244 filename = make_filename(INDEX_SESSION, sessionname);
245 fp = fopen(filename, "w");
246 if (!fp) {
247 *errmsg = dupprintf("Unable to save session: open(\"%s\") "
248 "returned '%s'", filename, strerror(errno));
249 sfree(filename);
250 return NULL; /* can't open */
251 }
252 sfree(filename);
253
254 settings_w *toret = snew(settings_w);
255 toret->fp = fp;
256 return toret;
257 }
258
write_setting_s(settings_w * handle,const char * key,const char * value)259 void write_setting_s(settings_w *handle, const char *key, const char *value)
260 {
261 fprintf(handle->fp, "%s=%s\n", key, value);
262 }
263
write_setting_i(settings_w * handle,const char * key,int value)264 void write_setting_i(settings_w *handle, const char *key, int value)
265 {
266 fprintf(handle->fp, "%s=%d\n", key, value);
267 }
268
close_settings_w(settings_w * handle)269 void close_settings_w(settings_w *handle)
270 {
271 fclose(handle->fp);
272 sfree(handle);
273 }
274
275 /* ----------------------------------------------------------------------
276 * System for treating X resources as a fallback source of defaults,
277 * after data read from a saved-session disk file.
278 *
279 * The read_setting_* functions will call get_setting(key) as a
280 * fallback if the setting isn't in the file they loaded. That in turn
281 * will hand on to x_get_default, which the front end application
282 * provides, and which actually reads resources from the X server (if
283 * appropriate). In between, there's a tree234 of X-resource shaped
284 * settings living locally in this file: the front end can call
285 * provide_xrm_string() to insert a setting into this tree (typically
286 * in response to an -xrm command line option or similar), and those
287 * will override the actual X resources.
288 */
289
290 struct skeyval {
291 const char *key;
292 const char *value;
293 };
294
295 static tree234 *xrmtree = NULL;
296
keycmp(void * av,void * bv)297 static int keycmp(void *av, void *bv)
298 {
299 struct skeyval *a = (struct skeyval *)av;
300 struct skeyval *b = (struct skeyval *)bv;
301 return strcmp(a->key, b->key);
302 }
303
provide_xrm_string(const char * string,const char * progname)304 void provide_xrm_string(const char *string, const char *progname)
305 {
306 const char *p, *q;
307 char *key;
308 struct skeyval *xrms, *ret;
309
310 p = q = strchr(string, ':');
311 if (!q) {
312 fprintf(stderr, "%s: expected a colon in resource string"
313 " \"%s\"\n", progname, string);
314 return;
315 }
316 q++;
317 while (p > string && p[-1] != '.' && p[-1] != '*')
318 p--;
319 xrms = snew(struct skeyval);
320 key = snewn(q-p, char);
321 memcpy(key, p, q-p);
322 key[q-p-1] = '\0';
323 xrms->key = key;
324 while (*q && isspace((unsigned char)*q))
325 q++;
326 xrms->value = dupstr(q);
327
328 if (!xrmtree)
329 xrmtree = newtree234(keycmp);
330
331 ret = add234(xrmtree, xrms);
332 if (ret) {
333 /* Override an existing string. */
334 del234(xrmtree, ret);
335 add234(xrmtree, xrms);
336 }
337 }
338
get_setting(const char * key)339 static const char *get_setting(const char *key)
340 {
341 struct skeyval tmp, *ret;
342 tmp.key = key;
343 if (xrmtree) {
344 ret = find234(xrmtree, &tmp, NULL);
345 if (ret)
346 return ret->value;
347 }
348 return x_get_default(key);
349 }
350
351 /* ----------------------------------------------------------------------
352 * Main code for reading settings from a disk file, calling the above
353 * get_setting() as a fallback if necessary.
354 */
355
356 struct settings_r {
357 tree234 *t;
358 };
359
open_settings_r(const char * sessionname)360 settings_r *open_settings_r(const char *sessionname)
361 {
362 char *filename;
363 FILE *fp;
364 char *line;
365 settings_r *toret;
366
367 filename = make_filename(INDEX_SESSION, sessionname);
368 fp = fopen(filename, "r");
369 sfree(filename);
370 if (!fp)
371 return NULL; /* can't open */
372
373 toret = snew(settings_r);
374 toret->t = newtree234(keycmp);
375
376 while ( (line = fgetline(fp)) ) {
377 char *value = strchr(line, '=');
378 struct skeyval *kv;
379
380 if (!value) {
381 sfree(line);
382 continue;
383 }
384 *value++ = '\0';
385 value[strcspn(value, "\r\n")] = '\0'; /* trim trailing NL */
386
387 kv = snew(struct skeyval);
388 kv->key = dupstr(line);
389 kv->value = dupstr(value);
390 add234(toret->t, kv);
391
392 sfree(line);
393 }
394
395 fclose(fp);
396
397 return toret;
398 }
399
read_setting_s(settings_r * handle,const char * key)400 char *read_setting_s(settings_r *handle, const char *key)
401 {
402 const char *val;
403 struct skeyval tmp, *kv;
404
405 tmp.key = key;
406 if (handle != NULL &&
407 (kv = find234(handle->t, &tmp, NULL)) != NULL) {
408 val = kv->value;
409 assert(val != NULL);
410 } else
411 val = get_setting(key);
412
413 if (!val)
414 return NULL;
415 else
416 return dupstr(val);
417 }
418
read_setting_i(settings_r * handle,const char * key,int defvalue)419 int read_setting_i(settings_r *handle, const char *key, int defvalue)
420 {
421 const char *val;
422 struct skeyval tmp, *kv;
423
424 tmp.key = key;
425 if (handle != NULL &&
426 (kv = find234(handle->t, &tmp, NULL)) != NULL) {
427 val = kv->value;
428 assert(val != NULL);
429 } else
430 val = get_setting(key);
431
432 if (!val)
433 return defvalue;
434 else
435 return atoi(val);
436 }
437
read_setting_fontspec(settings_r * handle,const char * name)438 FontSpec *read_setting_fontspec(settings_r *handle, const char *name)
439 {
440 /*
441 * In GTK1-only PuTTY, we used to store font names simply as a
442 * valid X font description string (logical or alias), under a
443 * bare key such as "Font".
444 *
445 * In GTK2 PuTTY, we have a prefix system where "client:"
446 * indicates a Pango font and "server:" an X one; existing
447 * configuration needs to be reinterpreted as having the
448 * "server:" prefix, so we change the storage key from the
449 * provided name string (e.g. "Font") to a suffixed one
450 * ("FontName").
451 */
452 char *suffname = dupcat(name, "Name");
453 char *tmp;
454
455 if ((tmp = read_setting_s(handle, suffname)) != NULL) {
456 FontSpec *fs = fontspec_new(tmp);
457 sfree(suffname);
458 sfree(tmp);
459 return fs; /* got new-style name */
460 }
461 sfree(suffname);
462
463 /* Fall back to old-style name. */
464 tmp = read_setting_s(handle, name);
465 if (tmp && *tmp) {
466 char *tmp2 = dupcat("server:", tmp);
467 FontSpec *fs = fontspec_new(tmp2);
468 sfree(tmp2);
469 sfree(tmp);
470 return fs;
471 } else {
472 sfree(tmp);
473 return NULL;
474 }
475 }
read_setting_filename(settings_r * handle,const char * name)476 Filename *read_setting_filename(settings_r *handle, const char *name)
477 {
478 char *tmp = read_setting_s(handle, name);
479 if (tmp) {
480 Filename *ret = filename_from_str(tmp);
481 sfree(tmp);
482 return ret;
483 } else
484 return NULL;
485 }
486
write_setting_fontspec(settings_w * handle,const char * name,FontSpec * fs)487 void write_setting_fontspec(settings_w *handle, const char *name, FontSpec *fs)
488 {
489 /*
490 * read_setting_fontspec had to handle two cases, but when
491 * writing our settings back out we simply always generate the
492 * new-style name.
493 */
494 char *suffname = dupcat(name, "Name");
495 write_setting_s(handle, suffname, fs->name);
496 sfree(suffname);
497 }
write_setting_filename(settings_w * handle,const char * name,Filename * result)498 void write_setting_filename(settings_w *handle,
499 const char *name, Filename *result)
500 {
501 write_setting_s(handle, name, result->path);
502 }
503
close_settings_r(settings_r * handle)504 void close_settings_r(settings_r *handle)
505 {
506 struct skeyval *kv;
507
508 if (!handle)
509 return;
510
511 while ( (kv = index234(handle->t, 0)) != NULL) {
512 del234(handle->t, kv);
513 sfree((char *)kv->key);
514 sfree((char *)kv->value);
515 sfree(kv);
516 }
517
518 freetree234(handle->t);
519 sfree(handle);
520 }
521
del_settings(const char * sessionname)522 void del_settings(const char *sessionname)
523 {
524 char *filename;
525 filename = make_filename(INDEX_SESSION, sessionname);
526 unlink(filename);
527 sfree(filename);
528 }
529
530 struct settings_e {
531 DIR *dp;
532 };
533
enum_settings_start(void)534 settings_e *enum_settings_start(void)
535 {
536 DIR *dp;
537 char *filename;
538
539 filename = make_filename(INDEX_SESSIONDIR, NULL);
540 dp = opendir(filename);
541 sfree(filename);
542
543 settings_e *toret = snew(settings_e);
544 toret->dp = dp;
545 return toret;
546 }
547
enum_settings_next(settings_e * handle,strbuf * out)548 bool enum_settings_next(settings_e *handle, strbuf *out)
549 {
550 struct dirent *de;
551 struct stat st;
552 strbuf *fullpath;
553
554 if (!handle->dp)
555 return NULL;
556
557 fullpath = strbuf_new();
558
559 char *sessiondir = make_filename(INDEX_SESSIONDIR, NULL);
560 put_datapl(fullpath, ptrlen_from_asciz(sessiondir));
561 sfree(sessiondir);
562 put_byte(fullpath, '/');
563
564 size_t baselen = fullpath->len;
565
566 while ( (de = readdir(handle->dp)) != NULL ) {
567 strbuf_shrink_to(fullpath, baselen);
568 put_datapl(fullpath, ptrlen_from_asciz(de->d_name));
569
570 if (stat(fullpath->s, &st) < 0 || !S_ISREG(st.st_mode))
571 continue; /* try another one */
572
573 decode_session_filename(de->d_name, out);
574 strbuf_free(fullpath);
575 return true;
576 }
577
578 strbuf_free(fullpath);
579 return false;
580 }
581
enum_settings_finish(settings_e * handle)582 void enum_settings_finish(settings_e *handle)
583 {
584 if (handle->dp)
585 closedir(handle->dp);
586 sfree(handle);
587 }
588
589 /*
590 * Lines in the host keys file are of the form
591 *
592 * type@port:hostname keydata
593 *
594 * e.g.
595 *
596 * rsa@22:foovax.example.org 0x23,0x293487364395345345....2343
597 */
verify_host_key(const char * hostname,int port,const char * keytype,const char * key)598 int verify_host_key(const char *hostname, int port,
599 const char *keytype, const char *key)
600 {
601 FILE *fp;
602 char *filename;
603 char *line;
604 int ret;
605
606 filename = make_filename(INDEX_HOSTKEYS, NULL);
607 fp = fopen(filename, "r");
608 sfree(filename);
609 if (!fp)
610 return 1; /* key does not exist */
611
612 ret = 1;
613 while ( (line = fgetline(fp)) ) {
614 int i;
615 char *p = line;
616 char porttext[20];
617
618 line[strcspn(line, "\n")] = '\0'; /* strip trailing newline */
619
620 i = strlen(keytype);
621 if (strncmp(p, keytype, i))
622 goto done;
623 p += i;
624
625 if (*p != '@')
626 goto done;
627 p++;
628
629 sprintf(porttext, "%d", port);
630 i = strlen(porttext);
631 if (strncmp(p, porttext, i))
632 goto done;
633 p += i;
634
635 if (*p != ':')
636 goto done;
637 p++;
638
639 i = strlen(hostname);
640 if (strncmp(p, hostname, i))
641 goto done;
642 p += i;
643
644 if (*p != ' ')
645 goto done;
646 p++;
647
648 /*
649 * Found the key. Now just work out whether it's the right
650 * one or not.
651 */
652 if (!strcmp(p, key))
653 ret = 0; /* key matched OK */
654 else
655 ret = 2; /* key mismatch */
656
657 done:
658 sfree(line);
659 if (ret != 1)
660 break;
661 }
662
663 fclose(fp);
664 return ret;
665 }
666
have_ssh_host_key(const char * hostname,int port,const char * keytype)667 bool have_ssh_host_key(const char *hostname, int port,
668 const char *keytype)
669 {
670 /*
671 * If we have a host key, verify_host_key will return 0 or 2.
672 * If we don't have one, it'll return 1.
673 */
674 return verify_host_key(hostname, port, keytype, "") != 1;
675 }
676
store_host_key(const char * hostname,int port,const char * keytype,const char * key)677 void store_host_key(const char *hostname, int port,
678 const char *keytype, const char *key)
679 {
680 FILE *rfp, *wfp;
681 char *newtext, *line;
682 int headerlen;
683 char *filename, *tmpfilename;
684
685 /*
686 * Open both the old file and a new file.
687 */
688 tmpfilename = make_filename(INDEX_HOSTKEYS_TMP, NULL);
689 wfp = fopen(tmpfilename, "w");
690 if (!wfp && errno == ENOENT) {
691 char *dir, *errmsg;
692
693 dir = make_filename(INDEX_DIR, NULL);
694 if ((errmsg = make_dir_path(dir, 0700)) != NULL) {
695 nonfatal("Unable to store host key: %s", errmsg);
696 sfree(errmsg);
697 sfree(dir);
698 sfree(tmpfilename);
699 return;
700 }
701 sfree(dir);
702
703 wfp = fopen(tmpfilename, "w");
704 }
705 if (!wfp) {
706 nonfatal("Unable to store host key: open(\"%s\") "
707 "returned '%s'", tmpfilename, strerror(errno));
708 sfree(tmpfilename);
709 return;
710 }
711 filename = make_filename(INDEX_HOSTKEYS, NULL);
712 rfp = fopen(filename, "r");
713
714 newtext = dupprintf("%s@%d:%s %s\n", keytype, port, hostname, key);
715 headerlen = 1 + strcspn(newtext, " "); /* count the space too */
716
717 /*
718 * Copy all lines from the old file to the new one that _don't_
719 * involve the same host key identifier as the one we're adding.
720 */
721 if (rfp) {
722 while ( (line = fgetline(rfp)) ) {
723 if (strncmp(line, newtext, headerlen))
724 fputs(line, wfp);
725 sfree(line);
726 }
727 fclose(rfp);
728 }
729
730 /*
731 * Now add the new line at the end.
732 */
733 fputs(newtext, wfp);
734
735 fclose(wfp);
736
737 if (rename(tmpfilename, filename) < 0) {
738 nonfatal("Unable to store host key: rename(\"%s\",\"%s\")"
739 " returned '%s'", tmpfilename, filename,
740 strerror(errno));
741 }
742
743 sfree(tmpfilename);
744 sfree(filename);
745 sfree(newtext);
746 }
747
read_random_seed(noise_consumer_t consumer)748 void read_random_seed(noise_consumer_t consumer)
749 {
750 int fd;
751 char *fname;
752
753 fname = make_filename(INDEX_RANDSEED, NULL);
754 fd = open(fname, O_RDONLY);
755 sfree(fname);
756 if (fd >= 0) {
757 char buf[512];
758 int ret;
759 while ( (ret = read(fd, buf, sizeof(buf))) > 0)
760 consumer(buf, ret);
761 close(fd);
762 }
763 }
764
write_random_seed(void * data,int len)765 void write_random_seed(void *data, int len)
766 {
767 int fd;
768 char *fname;
769
770 fname = make_filename(INDEX_RANDSEED, NULL);
771 /*
772 * Don't truncate the random seed file if it already exists; if
773 * something goes wrong half way through writing it, it would
774 * be better to leave the old data there than to leave it empty.
775 */
776 fd = open(fname, O_CREAT | O_WRONLY, 0600);
777 if (fd < 0) {
778 if (errno != ENOENT) {
779 nonfatal("Unable to write random seed: open(\"%s\") "
780 "returned '%s'", fname, strerror(errno));
781 sfree(fname);
782 return;
783 }
784 char *dir, *errmsg;
785
786 dir = make_filename(INDEX_DIR, NULL);
787 if ((errmsg = make_dir_path(dir, 0700)) != NULL) {
788 nonfatal("Unable to write random seed: %s", errmsg);
789 sfree(errmsg);
790 sfree(fname);
791 sfree(dir);
792 return;
793 }
794 sfree(dir);
795
796 fd = open(fname, O_CREAT | O_WRONLY, 0600);
797 if (fd < 0) {
798 nonfatal("Unable to write random seed: open(\"%s\") "
799 "returned '%s'", fname, strerror(errno));
800 sfree(fname);
801 return;
802 }
803 }
804
805 while (len > 0) {
806 int ret = write(fd, data, len);
807 if (ret < 0) {
808 nonfatal("Unable to write random seed: write "
809 "returned '%s'", strerror(errno));
810 break;
811 }
812 len -= ret;
813 data = (char *)data + len;
814 }
815
816 close(fd);
817 sfree(fname);
818 }
819
cleanup_all(void)820 void cleanup_all(void)
821 {
822 }
823