bba64e516f9691820d38bb3fef3386b22978489c
[platform/upstream/krb5.git] / src / lib / krb5 / ccache / cc_dir.c
1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /* lib/krb5/ccache/cc_dir.c - Directory-based credential cache collection */
3 /*
4  * Copyright (C) 2011 by the Massachusetts Institute of Technology.
5  * All rights reserved.
6  *
7  * Export of this software from the United States of America may
8  *   require a specific license from the United States Government.
9  *   It is the responsibility of any person or organization contemplating
10  *   export to obtain such a license before exporting.
11  *
12  * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
13  * distribute this software and its documentation for any purpose and
14  * without fee is hereby granted, provided that the above copyright
15  * notice appear in all copies and that both that copyright notice and
16  * this permission notice appear in supporting documentation, and that
17  * the name of M.I.T. not be used in advertising or publicity pertaining
18  * to distribution of the software without specific, written prior
19  * permission.  Furthermore if you modify this software you must label
20  * your software as modified software and not distribute it in such a
21  * fashion that it might be confused with the original M.I.T. software.
22  * M.I.T. makes no representations about the suitability of
23  * this software for any purpose.  It is provided "as is" without express
24  * or implied warranty.
25  */
26
27 /*
28  * This credential cache type represents a set of file-based caches with a
29  * switchable primary cache.  An alternate form of the type represents a
30  * subsidiary file cache within the directory.
31  *
32  * A cache name of the form DIR:dirname identifies a directory containing the
33  * cache set.  Resolving a name of this form results in dirname's primary
34  * cache.  If a context's default cache is of this form, the global cache
35  * collection will contain dirname's cache set, and new unique caches of type
36  * DIR will be created within dirname.
37  *
38  * A cache name of the form DIR::filepath represents a single cache within the
39  * directory.  Switching to a ccache of this type causes the directory's
40  * primary cache to be set to the named cache.
41  *
42  * Within the directory, cache names begin with 'tkt'.  The file "primary"
43  * contains a single line naming the primary cache.  The directory must already
44  * exist when the DIR ccache is resolved, but the primary file will be created
45  * automatically if it does not exist.
46  */
47
48 #include "k5-int.h"
49 #include "cc-int.h"
50
51 #if HAVE_UNISTD_H
52 #include <unistd.h>
53 #endif
54 #if HAVE_SYS_STAT_H
55 #include <sys/stat.h>
56 #endif
57
58 /* This is Unix-only for now.  To work on Windows, we will need opendir/readdir
59  * replacements and possibly more flexible newline handling. */
60 #ifndef _WIN32
61
62 #include <dirent.h>
63
64 extern const krb5_cc_ops krb5_dcc_ops;
65 extern const krb5_cc_ops krb5_fcc_ops;
66
67 /* Fields are not modified after creation, so no lock is necessary. */
68 typedef struct dcc_data_st {
69     char *residual;             /* dirname or :filename */
70     krb5_ccache fcc;            /* File cache for actual cache ops */
71 } dcc_data;
72
73 static inline krb5_boolean
74 filename_is_cache(const char *filename)
75 {
76     return (strncmp(filename, "tkt", 3) == 0);
77 }
78
79 /* Compose the pathname of the primary file within a cache directory. */
80 static inline krb5_error_code
81 primary_pathname(const char *dirname, char **path_out)
82 {
83     return k5_path_join(dirname, "primary", path_out);
84 }
85
86 /* Compose a residual string for a subsidiary path with the specified directory
87  * name and filename. */
88 static krb5_error_code
89 subsidiary_residual(const char *dirname, const char *filename, char **out)
90 {
91     krb5_error_code ret;
92     char *path, *residual;
93
94     *out = NULL;
95     ret = k5_path_join(dirname, filename, &path);
96     if (ret)
97         return ret;
98     ret = asprintf(&residual, ":%s", path);
99     free(path);
100     if (ret < 0)
101         return ENOMEM;
102     *out = residual;
103     return 0;
104 }
105
106 static inline krb5_error_code
107 split_path(krb5_context context, const char *path, char **dirname_out,
108            char **filename_out)
109 {
110     krb5_error_code ret;
111     char *dirname, *filename;
112
113     *dirname_out = NULL;
114     *filename_out = NULL;
115     ret = k5_path_split(path, &dirname, &filename);
116     if (ret)
117         return ret;
118
119     if (*dirname == '\0') {
120         ret = KRB5_CC_BADNAME;
121         k5_setmsg(context, ret,
122                   _("Subsidiary cache path %s has no parent directory"), path);
123         goto error;
124     }
125     if (!filename_is_cache(filename)) {
126         ret = KRB5_CC_BADNAME;
127         k5_setmsg(context, ret,
128                   _("Subsidiary cache path %s filename does not begin with "
129                     "\"tkt\""), path);
130         goto error;
131     }
132
133     *dirname_out = dirname;
134     *filename_out = filename;
135     return 0;
136
137 error:
138     free(dirname);
139     free(filename);
140     return ret;
141 }
142
143 /* Read the primary file and compose the residual string for the primary
144  * subsidiary cache file. */
145 static krb5_error_code
146 read_primary_file(krb5_context context, const char *primary_path,
147                   const char *dirname, char **residual_out)
148 {
149     FILE *fp;
150     char buf[64], *ret;
151     size_t len;
152
153     *residual_out = NULL;
154
155     /* Open the file and read its first line. */
156     fp = fopen(primary_path, "r");
157     if (fp == NULL)
158         return ENOENT;
159     ret = fgets(buf, sizeof(buf), fp);
160     fclose(fp);
161     if (ret == NULL)
162         return KRB5_CC_IO;
163     len = strlen(buf);
164
165     /* Check if line is too long, doesn't look like a subsidiary cache
166      * filename, or isn't a single-component filename. */
167     if (buf[len - 1] != '\n' || !filename_is_cache(buf) ||
168         strchr(buf, '/') || strchr(buf, '\\')) {
169         k5_setmsg(context, KRB5_CC_FORMAT, _("%s contains invalid filename"),
170                   primary_path);
171         return KRB5_CC_FORMAT;
172     }
173     buf[len - 1] = '\0';
174
175     return subsidiary_residual(dirname, buf, residual_out);
176 }
177
178 /* Create or update the primary file with a line containing contents. */
179 static krb5_error_code
180 write_primary_file(const char *primary_path, const char *contents)
181 {
182     krb5_error_code ret = KRB5_CC_IO;
183     char *newpath = NULL;
184     FILE *fp = NULL;
185     int fd = -1, status;
186
187     if (asprintf(&newpath, "%s.XXXXXX", primary_path) < 0)
188         return ENOMEM;
189     fd = mkstemp(newpath);
190     if (fd < 0)
191         goto cleanup;
192 #ifdef HAVE_CHMOD
193     chmod(newpath, S_IRUSR | S_IWUSR);
194 #endif
195     fp = fdopen(fd, "w");
196     if (fp == NULL)
197         goto cleanup;
198     fd = -1;
199     if (fprintf(fp, "%s\n", contents) < 0)
200         goto cleanup;
201     status = fclose(fp);
202     fp = NULL;
203     if (status == EOF)
204         goto cleanup;
205     fp = NULL;
206     if (rename(newpath, primary_path) != 0)
207         goto cleanup;
208     ret = 0;
209
210 cleanup:
211     if (fd >= 0)
212         close(fd);
213     if (fp != NULL)
214         fclose(fp);
215     free(newpath);
216     return ret;
217 }
218
219 /* Verify or create a cache directory path. */
220 static krb5_error_code
221 verify_dir(krb5_context context, const char *dirname)
222 {
223     struct stat st;
224
225     if (stat(dirname, &st) < 0) {
226         if (errno == ENOENT && mkdir(dirname, S_IRWXU) == 0)
227             return 0;
228         k5_setmsg(context, KRB5_FCC_NOFILE,
229                   _("Credential cache directory %s does not exist"),
230                   dirname);
231         return KRB5_FCC_NOFILE;
232     }
233     if (!S_ISDIR(st.st_mode)) {
234         k5_setmsg(context, KRB5_CC_FORMAT,
235                   _("Credential cache directory %s exists but is not a "
236                     "directory"), dirname);
237         return KRB5_CC_FORMAT;
238     }
239     return 0;
240 }
241
242 /*
243  * If the default ccache name for context is a directory collection, set
244  * *dirname_out to the directory name for that collection.  Otherwise set
245  * *dirname_out to NULL.
246  */
247 static krb5_error_code
248 get_context_default_dir(krb5_context context, char **dirname_out)
249 {
250     const char *defname;
251     char *dirname;
252
253     *dirname_out = NULL;
254     defname = krb5_cc_default_name(context);
255     if (defname == NULL)
256         return 0;
257     if (strncmp(defname, "DIR:", 4) != 0 ||
258         defname[4] == ':' || defname[4] == '\0')
259         return 0;
260     dirname = strdup(defname + 4);
261     if (dirname == NULL)
262         return ENOMEM;
263     *dirname_out = dirname;
264     return 0;
265 }
266
267 /*
268  * If the default ccache name for context is a subsidiary file in a directory
269  * collection, set *subsidiary_out to the residual value.  Otherwise set
270  * *subsidiary_out to NULL.
271  */
272 static krb5_error_code
273 get_context_subsidiary_file(krb5_context context, char **subsidiary_out)
274 {
275     const char *defname;
276     char *residual;
277
278     *subsidiary_out = NULL;
279     defname = krb5_cc_default_name(context);
280     if (defname == NULL || strncmp(defname, "DIR::", 5) != 0)
281         return 0;
282     residual = strdup(defname + 4);
283     if (residual == NULL)
284         return ENOMEM;
285     *subsidiary_out = residual;
286     return 0;
287 }
288
289 static const char * KRB5_CALLCONV
290 dcc_get_name(krb5_context context, krb5_ccache cache)
291 {
292     dcc_data *data = cache->data;
293
294     return data->residual;
295 }
296
297 /* Construct a cache object given a residual string and file ccache.  Take
298  * ownership of fcc on success. */
299 static krb5_error_code
300 make_cache(const char *residual, krb5_ccache fcc, krb5_ccache *cache_out)
301 {
302     krb5_ccache cache = NULL;
303     dcc_data *data = NULL;
304     char *residual_copy = NULL;
305
306     cache = malloc(sizeof(*cache));
307     if (cache == NULL)
308         goto oom;
309     data = malloc(sizeof(*data));
310     if (data == NULL)
311         goto oom;
312     residual_copy = strdup(residual);
313     if (residual_copy == NULL)
314         goto oom;
315
316     data->residual = residual_copy;
317     data->fcc = fcc;
318     cache->ops = &krb5_dcc_ops;
319     cache->data = data;
320     cache->magic = KV5M_CCACHE;
321     *cache_out = cache;
322     return 0;
323
324 oom:
325     free(cache);
326     free(data);
327     free(residual_copy);
328     return ENOMEM;
329 }
330
331 static krb5_error_code KRB5_CALLCONV
332 dcc_resolve(krb5_context context, krb5_ccache *cache_out, const char *residual)
333 {
334     krb5_error_code ret;
335     krb5_ccache fcc;
336     char *primary_path = NULL, *sresidual = NULL, *dirname, *filename;
337
338     *cache_out = NULL;
339
340     if (*residual == ':') {
341         /* This is a subsidiary cache within the directory. */
342         ret = split_path(context, residual + 1, &dirname, &filename);
343         if (ret)
344             return ret;
345
346         ret = verify_dir(context, dirname);
347         free(dirname);
348         free(filename);
349         if (ret)
350             return ret;
351     } else {
352         /* This is the directory itself; resolve to the primary cache. */
353         ret = verify_dir(context, residual);
354         if (ret)
355             return ret;
356
357         ret = primary_pathname(residual, &primary_path);
358         if (ret)
359             goto cleanup;
360
361         ret = read_primary_file(context, primary_path, residual, &sresidual);
362         if (ret == ENOENT) {
363             /* Create an initial primary file. */
364             ret = write_primary_file(primary_path, "tkt");
365             if (ret)
366                 goto cleanup;
367             ret = subsidiary_residual(residual, "tkt", &sresidual);
368         }
369         if (ret)
370             goto cleanup;
371         residual = sresidual;
372     }
373
374     ret = krb5_fcc_ops.resolve(context, &fcc, residual + 1);
375     if (ret)
376         goto cleanup;
377     ret = make_cache(residual, fcc, cache_out);
378     if (ret)
379         krb5_fcc_ops.close(context, fcc);
380
381 cleanup:
382     free(primary_path);
383     free(sresidual);
384     return ret;
385 }
386
387 static krb5_error_code KRB5_CALLCONV
388 dcc_gen_new(krb5_context context, krb5_ccache *cache_out)
389 {
390     krb5_error_code ret;
391     char *dirname = NULL, *template = NULL, *residual = NULL;
392     krb5_ccache fcc = NULL;
393
394     *cache_out = NULL;
395     ret = get_context_default_dir(context, &dirname);
396     if (ret)
397         return ret;
398     if (dirname == NULL) {
399         k5_setmsg(context, KRB5_DCC_CANNOT_CREATE,
400                   _("Can't create new subsidiary cache because default cache "
401                     "is not a directory collection"));
402         return KRB5_DCC_CANNOT_CREATE;
403     }
404     ret = verify_dir(context, dirname);
405     if (ret)
406         goto cleanup;
407     ret = k5_path_join(dirname, "tktXXXXXX", &template);
408     if (ret)
409         goto cleanup;
410     ret = krb5int_fcc_new_unique(context, template, &fcc);
411     if (ret)
412         goto cleanup;
413     if (asprintf(&residual, ":%s", template) < 0) {
414         ret = ENOMEM;
415         goto cleanup;
416     }
417     ret = make_cache(residual, fcc, cache_out);
418     if (ret)
419         goto cleanup;
420     fcc = NULL;
421
422 cleanup:
423     if (fcc != NULL)
424         krb5_fcc_ops.destroy(context, fcc);
425     free(dirname);
426     free(template);
427     free(residual);
428     return ret;
429 }
430
431 static krb5_error_code KRB5_CALLCONV
432 dcc_init(krb5_context context, krb5_ccache cache, krb5_principal princ)
433 {
434     dcc_data *data = cache->data;
435
436     return krb5_fcc_ops.init(context, data->fcc, princ);
437 }
438
439 static krb5_error_code KRB5_CALLCONV
440 dcc_destroy(krb5_context context, krb5_ccache cache)
441 {
442     dcc_data *data = cache->data;
443     krb5_error_code ret;
444
445     ret = krb5_fcc_ops.destroy(context, data->fcc);
446     free(data->residual);
447     free(data);
448     free(cache);
449     return ret;
450 }
451
452 static krb5_error_code KRB5_CALLCONV
453 dcc_close(krb5_context context, krb5_ccache cache)
454 {
455     dcc_data *data = cache->data;
456     krb5_error_code ret;
457
458     ret = krb5_fcc_ops.close(context, data->fcc);
459     free(data->residual);
460     free(data);
461     free(cache);
462     return ret;
463 }
464
465 static krb5_error_code KRB5_CALLCONV
466 dcc_store(krb5_context context, krb5_ccache cache, krb5_creds *creds)
467 {
468     dcc_data *data = cache->data;
469
470     return krb5_fcc_ops.store(context, data->fcc, creds);
471 }
472
473 static krb5_error_code KRB5_CALLCONV
474 dcc_retrieve(krb5_context context, krb5_ccache cache, krb5_flags flags,
475              krb5_creds *mcreds, krb5_creds *creds)
476 {
477     dcc_data *data = cache->data;
478
479     return krb5_fcc_ops.retrieve(context, data->fcc, flags, mcreds,
480                                  creds);
481 }
482
483 static krb5_error_code KRB5_CALLCONV
484 dcc_get_princ(krb5_context context, krb5_ccache cache,
485               krb5_principal *princ_out)
486 {
487     dcc_data *data = cache->data;
488
489     return krb5_fcc_ops.get_princ(context, data->fcc, princ_out);
490 }
491
492 static krb5_error_code KRB5_CALLCONV
493 dcc_get_first(krb5_context context, krb5_ccache cache, krb5_cc_cursor *cursor)
494 {
495     dcc_data *data = cache->data;
496
497     return krb5_fcc_ops.get_first(context, data->fcc, cursor);
498 }
499
500 static krb5_error_code KRB5_CALLCONV
501 dcc_get_next(krb5_context context, krb5_ccache cache, krb5_cc_cursor *cursor,
502              krb5_creds *creds)
503 {
504     dcc_data *data = cache->data;
505
506     return krb5_fcc_ops.get_next(context, data->fcc, cursor, creds);
507 }
508
509 static krb5_error_code KRB5_CALLCONV
510 dcc_end_get(krb5_context context, krb5_ccache cache, krb5_cc_cursor *cursor)
511 {
512     dcc_data *data = cache->data;
513
514     return krb5_fcc_ops.end_get(context, data->fcc, cursor);
515 }
516
517 static krb5_error_code KRB5_CALLCONV
518 dcc_remove_cred(krb5_context context, krb5_ccache cache, krb5_flags flags,
519                 krb5_creds *creds)
520 {
521     dcc_data *data = cache->data;
522
523     return krb5_fcc_ops.remove_cred(context, data->fcc, flags, creds);
524 }
525
526 static krb5_error_code KRB5_CALLCONV
527 dcc_set_flags(krb5_context context, krb5_ccache cache, krb5_flags flags)
528 {
529     dcc_data *data = cache->data;
530
531     return krb5_fcc_ops.set_flags(context, data->fcc, flags);
532 }
533
534 static krb5_error_code KRB5_CALLCONV
535 dcc_get_flags(krb5_context context, krb5_ccache cache, krb5_flags *flags_out)
536 {
537     dcc_data *data = cache->data;
538
539     return krb5_fcc_ops.get_flags(context, data->fcc, flags_out);
540 }
541
542 struct dcc_ptcursor_data {
543     char *primary;
544     char *dirname;
545     DIR *dir;
546     krb5_boolean first;
547 };
548
549 /* Construct a cursor, taking ownership of dirname, primary, and dir on
550  * success. */
551 static krb5_error_code
552 make_cursor(char *dirname, char *primary, DIR *dir,
553             krb5_cc_ptcursor *cursor_out)
554 {
555     krb5_cc_ptcursor cursor;
556     struct dcc_ptcursor_data *data;
557
558     *cursor_out = NULL;
559
560     data = malloc(sizeof(*data));
561     if (data == NULL)
562         return ENOMEM;
563     cursor = malloc(sizeof(*cursor));
564     if (cursor == NULL) {
565         free(data);
566         return ENOMEM;
567     }
568
569     data->dirname = dirname;
570     data->primary = primary;
571     data->dir = dir;
572     data->first = TRUE;
573     cursor->ops = &krb5_dcc_ops;
574     cursor->data = data;
575     *cursor_out = cursor;
576     return 0;
577 }
578
579 static krb5_error_code KRB5_CALLCONV
580 dcc_ptcursor_new(krb5_context context, krb5_cc_ptcursor *cursor_out)
581 {
582     krb5_error_code ret;
583     char *dirname = NULL, *primary_path = NULL, *primary = NULL;
584     DIR *dir = NULL;
585
586     *cursor_out = NULL;
587
588     /* If the default cache is a subsidiary file, make a cursor with the
589      * specified file as the primary but with no directory collection. */
590     ret = get_context_subsidiary_file(context, &primary);
591     if (ret)
592         goto cleanup;
593     if (primary != NULL) {
594         ret = make_cursor(NULL, primary, NULL, cursor_out);
595         if (ret)
596             free(primary);
597         return ret;
598     }
599
600     /* Open the directory for the context's default cache. */
601     ret = get_context_default_dir(context, &dirname);
602     if (ret || dirname == NULL)
603         goto cleanup;
604     dir = opendir(dirname);
605     if (dir == NULL)
606         goto cleanup;
607
608     /* Fetch the primary cache name if possible. */
609     ret = primary_pathname(dirname, &primary_path);
610     if (ret)
611         goto cleanup;
612     ret = read_primary_file(context, primary_path, dirname, &primary);
613     if (ret)
614         krb5_clear_error_message(context);
615
616     ret = make_cursor(dirname, primary, dir, cursor_out);
617     if (ret)
618         goto cleanup;
619     dirname = primary = NULL;
620     dir = NULL;
621
622 cleanup:
623     free(dirname);
624     free(primary_path);
625     free(primary);
626     if (dir)
627         closedir(dir);
628     /* Return an empty cursor if we fail for any reason. */
629     if (*cursor_out == NULL)
630         return make_cursor(NULL, NULL, NULL, cursor_out);
631     return 0;
632 }
633
634 static krb5_error_code KRB5_CALLCONV
635 dcc_ptcursor_next(krb5_context context, krb5_cc_ptcursor cursor,
636                   krb5_ccache *cache_out)
637 {
638     struct dcc_ptcursor_data *data = cursor->data;
639     struct dirent *ent;
640     char *residual;
641     krb5_error_code ret;
642     struct stat sb;
643
644     *cache_out = NULL;
645
646     /* Return the primary or specified subsidiary cache if we haven't yet. */
647     if (data->first) {
648         data->first = FALSE;
649         if (data->primary != NULL && stat(data->primary + 1, &sb) == 0)
650             return dcc_resolve(context, cache_out, data->primary);
651     }
652
653     if (data->dir == NULL)      /* No directory collection */
654         return 0;
655
656     /* Look for the next filename of the correct form, without repeating the
657      * primary cache. */
658     while ((ent = readdir(data->dir)) != NULL) {
659         if (!filename_is_cache(ent->d_name))
660             continue;
661         ret = subsidiary_residual(data->dirname, ent->d_name, &residual);
662         if (ret)
663             return ret;
664         if (data->primary != NULL && strcmp(residual, data->primary) == 0) {
665             free(residual);
666             continue;
667         }
668         ret = dcc_resolve(context, cache_out, residual);
669         free(residual);
670         return ret;
671     }
672
673     /* We exhausted the directory without finding a cache to yield. */
674     closedir(data->dir);
675     data->dir = NULL;
676     return 0;
677 }
678
679 static krb5_error_code KRB5_CALLCONV
680 dcc_ptcursor_free(krb5_context context, krb5_cc_ptcursor *cursor)
681 {
682     struct dcc_ptcursor_data *data = (*cursor)->data;
683
684     if (data->dir)
685         closedir(data->dir);
686     free(data->dirname);
687     free(data->primary);
688     free(data);
689     free(*cursor);
690     *cursor = NULL;
691     return 0;
692 }
693
694 static krb5_error_code KRB5_CALLCONV
695 dcc_lastchange(krb5_context context, krb5_ccache cache,
696                krb5_timestamp *time_out)
697 {
698     dcc_data *data = cache->data;
699
700     return krb5_fcc_ops.lastchange(context, data->fcc, time_out);
701 }
702
703 static krb5_error_code KRB5_CALLCONV
704 dcc_lock(krb5_context context, krb5_ccache cache)
705 {
706     dcc_data *data = cache->data;
707
708     return krb5_fcc_ops.lock(context, data->fcc);
709 }
710
711 static krb5_error_code KRB5_CALLCONV
712 dcc_unlock(krb5_context context, krb5_ccache cache)
713 {
714     dcc_data *data = cache->data;
715
716     return krb5_fcc_ops.unlock(context, data->fcc);
717 }
718
719 static krb5_error_code KRB5_CALLCONV
720 dcc_switch_to(krb5_context context, krb5_ccache cache)
721 {
722     dcc_data *data = cache->data;
723     char *primary_path = NULL, *dirname = NULL, *filename = NULL;
724     krb5_error_code ret;
725
726     ret = split_path(context, data->residual + 1, &dirname, &filename);
727     if (ret)
728         return ret;
729
730     ret = primary_pathname(dirname, &primary_path);
731     if (ret)
732         goto cleanup;
733
734     ret = write_primary_file(primary_path, filename);
735
736 cleanup:
737     free(primary_path);
738     free(dirname);
739     free(filename);
740     return ret;
741 }
742
743 const krb5_cc_ops krb5_dcc_ops = {
744     0,
745     "DIR",
746     dcc_get_name,
747     dcc_resolve,
748     dcc_gen_new,
749     dcc_init,
750     dcc_destroy,
751     dcc_close,
752     dcc_store,
753     dcc_retrieve,
754     dcc_get_princ,
755     dcc_get_first,
756     dcc_get_next,
757     dcc_end_get,
758     dcc_remove_cred,
759     dcc_set_flags,
760     dcc_get_flags,
761     dcc_ptcursor_new,
762     dcc_ptcursor_next,
763     dcc_ptcursor_free,
764     NULL, /* move */
765     dcc_lastchange,
766     NULL, /* wasdefault */
767     dcc_lock,
768     dcc_unlock,
769     dcc_switch_to,
770 };
771
772 #endif /* not _WIN32 */