xref: /netbsd-src/usr.sbin/edquota/edquota.c (revision f386908b273c66be8430ac51f71a66ae1d59dc37)
1 /*      $NetBSD: edquota.c,v 1.54 2023/08/01 08:47:25 mrg Exp $ */
2 /*
3  * Copyright (c) 1980, 1990, 1993
4  *	The Regents of the University of California.  All rights reserved.
5  *
6  * This code is derived from software contributed to Berkeley by
7  * Robert Elz at The University of Melbourne.
8  *
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions
11  * are met:
12  * 1. Redistributions of source code must retain the above copyright
13  *    notice, this list of conditions and the following disclaimer.
14  * 2. Redistributions in binary form must reproduce the above copyright
15  *    notice, this list of conditions and the following disclaimer in the
16  *    documentation and/or other materials provided with the distribution.
17  * 3. Neither the name of the University nor the names of its contributors
18  *    may be used to endorse or promote products derived from this software
19  *    without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
22  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
25  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31  * SUCH DAMAGE.
32  */
33 
34 #include <sys/cdefs.h>
35 #ifndef lint
36 __COPYRIGHT("@(#) Copyright (c) 1980, 1990, 1993\
37  The Regents of the University of California.  All rights reserved.");
38 #endif /* not lint */
39 
40 #ifndef lint
41 #if 0
42 static char sccsid[] = "from: @(#)edquota.c	8.3 (Berkeley) 4/27/95";
43 #else
44 __RCSID("$NetBSD: edquota.c,v 1.54 2023/08/01 08:47:25 mrg Exp $");
45 #endif
46 #endif /* not lint */
47 
48 /*
49  * Disk quota editor.
50  */
51 #include <sys/param.h>
52 #include <sys/stat.h>
53 #include <sys/file.h>
54 #include <sys/wait.h>
55 #include <sys/queue.h>
56 #include <sys/types.h>
57 #include <sys/statvfs.h>
58 
59 #include <quota.h>
60 
61 #include <assert.h>
62 #include <err.h>
63 #include <errno.h>
64 #include <fstab.h>
65 #include <pwd.h>
66 #include <grp.h>
67 #include <ctype.h>
68 #include <signal.h>
69 #include <stdbool.h>
70 #include <stdio.h>
71 #include <stdlib.h>
72 #include <string.h>
73 #include <unistd.h>
74 #include <util.h>
75 
76 #include "printquota.h"
77 
78 #include "pathnames.h"
79 
80 /*
81  * XXX. Ideally we shouldn't compile this in, but it'll take some
82  * reworking to avoid it and it'll be ok for now.
83  */
84 #define EDQUOTA_NUMOBJTYPES	2
85 
86 #if 0
87 static const char *quotagroup = QUOTAGROUP;
88 #endif
89 
90 #define MAX_TMPSTR	(100+MAXPATHLEN)
91 
92 enum sources {
93 	SRC_EDITED,	/* values came from user */
94 	SRC_QUOTA,	/* values came from a specific quota entry */
95 	SRC_DEFAULT,	/* values were copied from the default quota entry */
96 	SRC_CLEAR,	/* values arose by zeroing out a quota entry */
97 };
98 
99 struct quotause {
100 	struct	quotause *next;
101 	unsigned found:1,	/* found after running editor */
102 		xgrace:1,	/* grace periods are per-id */
103 		isdefault:1;
104 
105 	struct	quotaval qv[EDQUOTA_NUMOBJTYPES];
106 	enum sources source[EDQUOTA_NUMOBJTYPES];
107 	char	fsname[MAXPATHLEN + 1];
108 	char	implementation[32];
109 };
110 
111 struct quotalist {
112 	struct quotause *head;
113 	struct quotause *tail;
114 	char *idtypename;
115 };
116 
117 static void	usage(void) __dead;
118 
119 static int Hflag = 0;
120 
121 /* more compact form of constants */
122 #define QO_BLK QUOTA_OBJTYPE_BLOCKS
123 #define QO_FL  QUOTA_OBJTYPE_FILES
124 
125 ////////////////////////////////////////////////////////////
126 // support code
127 
128 /*
129  * This routine converts a name for a particular quota class to
130  * an identifier. This routine must agree with the kernel routine
131  * getinoquota as to the interpretation of quota classes.
132  */
133 static int
getidbyname(const char * name,int idtype)134 getidbyname(const char *name, int idtype)
135 {
136 	struct passwd *pw;
137 	struct group *gr;
138 
139 	if (alldigits(name))
140 		return atoi(name);
141 	switch (idtype) {
142 	case QUOTA_IDTYPE_USER:
143 		if ((pw = getpwnam(name)) != NULL)
144 			return pw->pw_uid;
145 		warnx("%s: no such user", name);
146 		break;
147 	case QUOTA_IDTYPE_GROUP:
148 		if ((gr = getgrnam(name)) != NULL)
149 			return gr->gr_gid;
150 		warnx("%s: no such group", name);
151 		break;
152 	default:
153 		warnx("%d: unknown quota type", idtype);
154 		break;
155 	}
156 	sleep(1);
157 	return -1;
158 }
159 
160 /*
161  * check if a source is "real" (reflects actual data) or not
162  */
163 static bool
source_is_real(enum sources source)164 source_is_real(enum sources source)
165 {
166 	switch (source) {
167 	    case SRC_EDITED:
168 	    case SRC_QUOTA:
169 		return true;
170 	    case SRC_DEFAULT:
171 	    case SRC_CLEAR:
172 		return false;
173 	}
174 	assert(!"encountered invalid source");
175 	return false;
176 }
177 
178 /*
179  * some simple string tools
180  */
181 
182 static /*const*/ char *
skipws(char * s)183 skipws(/*const*/ char *s)
184 {
185 	while (*s == ' ' || *s == '\t') {
186 		s++;
187 	}
188 	return s;
189 }
190 
191 static /*const*/ char *
skipword(char * s)192 skipword(/*const*/ char *s)
193 {
194 	while (*s != '\0' && *s != '\n' && *s != ' ' && *s != '\t') {
195 		s++;
196 	}
197 	return s;
198 }
199 
200 ////////////////////////////////////////////////////////////
201 // quotause operations
202 
203 /*
204  * Create an empty quotause structure.
205  */
206 static struct quotause *
quotause_create(void)207 quotause_create(void)
208 {
209 	struct quotause *qup;
210 	unsigned i;
211 
212 	qup = malloc(sizeof(*qup));
213 	if (qup == NULL) {
214 		err(1, "malloc");
215 	}
216 
217 	qup->next = NULL;
218 	qup->found = 0;
219 	qup->xgrace = 0;
220 	qup->isdefault = 0;
221 	for (i=0; i<EDQUOTA_NUMOBJTYPES; i++) {
222 		quotaval_clear(&qup->qv[i]);
223 		qup->source[i] = SRC_CLEAR;
224 	}
225 	qup->fsname[0] = '\0';
226 
227 	return qup;
228 }
229 
230 /*
231  * Free a quotause structure.
232  */
233 static void
quotause_destroy(struct quotause * qup)234 quotause_destroy(struct quotause *qup)
235 {
236 	free(qup);
237 }
238 
239 ////////////////////////////////////////////////////////////
240 // quotalist operations
241 
242 /*
243  * Create a quotause list.
244  */
245 static struct quotalist *
quotalist_create(void)246 quotalist_create(void)
247 {
248 	struct quotalist *qlist;
249 
250 	qlist = malloc(sizeof(*qlist));
251 	if (qlist == NULL) {
252 		err(1, "malloc");
253 	}
254 
255 	qlist->head = NULL;
256 	qlist->tail = NULL;
257 	qlist->idtypename = NULL;
258 
259 	return qlist;
260 }
261 
262 /*
263  * Free a list of quotause structures.
264  */
265 static void
quotalist_destroy(struct quotalist * qlist)266 quotalist_destroy(struct quotalist *qlist)
267 {
268 	struct quotause *qup, *nextqup;
269 
270 	for (qup = qlist->head; qup; qup = nextqup) {
271 		nextqup = qup->next;
272 		quotause_destroy(qup);
273 	}
274 	free(qlist->idtypename);
275 	free(qlist);
276 }
277 
278 #if 0
279 static bool
280 quotalist_empty(struct quotalist *qlist)
281 {
282 	return qlist->head == NULL;
283 }
284 #endif
285 
286 static void
quotalist_append(struct quotalist * qlist,struct quotause * qup)287 quotalist_append(struct quotalist *qlist, struct quotause *qup)
288 {
289 	/* should not already be on a list */
290 	assert(qup->next == NULL);
291 
292 	if (qlist->head == NULL) {
293 		qlist->head = qup;
294 	} else {
295 		qlist->tail->next = qup;
296 	}
297 	qlist->tail = qup;
298 }
299 
300 ////////////////////////////////////////////////////////////
301 // ffs quota v1
302 
303 #if 0
304 static void
305 putprivs1(uint32_t id, int idtype, struct quotause *qup)
306 {
307 	struct dqblk dqblk;
308 	int fd;
309 
310 	quotavals_to_dqblk(&qup->qv[QUOTA_LIMIT_BLOCK],
311 			   &qup->qv[QUOTA_LIMIT_FILE],
312 			   &dqblk);
313 	assert((qup->flags & DEFAULT) == 0);
314 
315 	if ((fd = open(qup->qfname, O_WRONLY)) < 0) {
316 		warnx("open `%s'", qup->qfname);
317 	} else {
318 		(void)lseek(fd,
319 		    (off_t)(id * (long)sizeof (struct dqblk)),
320 		    SEEK_SET);
321 		if (write(fd, &dqblk, sizeof (struct dqblk)) !=
322 		    sizeof (struct dqblk))
323 			warnx("writing `%s'", qup->qfname);
324 		close(fd);
325 	}
326 }
327 
328 static struct quotause *
329 getprivs1(long id, int idtype, const char *filesys)
330 {
331 	struct fstab *fs;
332 	char qfpathname[MAXPATHLEN], xbuf[MAXPATHLEN];
333 	const char *fsspec;
334 	struct quotause *qup;
335 	struct dqblk dqblk;
336 	int fd;
337 
338 	setfsent();
339 	while ((fs = getfsent()) != NULL) {
340 		if (strcmp(fs->fs_vfstype, "ffs"))
341 			continue;
342 		fsspec = getfsspecname(xbuf, sizeof(xbuf), fs->fs_spec);
343 		if (fsspec == NULL) {
344 			warn("%s", xbuf);
345 			continue;
346 		}
347 		if (strcmp(fsspec, filesys) == 0 ||
348 		    strcmp(fs->fs_file, filesys) == 0)
349 			break;
350 	}
351 	if (fs == NULL)
352 		return NULL;
353 
354 	if (!hasquota(qfpathname, sizeof(qfpathname), fs,
355 	    quota_idtype_to_ufs(idtype)))
356 		return NULL;
357 
358 	qup = quotause_create();
359 	strcpy(qup->fsname, fs->fs_file);
360 	if ((fd = open(qfpathname, O_RDONLY)) < 0) {
361 		fd = open(qfpathname, O_RDWR|O_CREAT, 0640);
362 		if (fd < 0 && errno != ENOENT) {
363 			warnx("open `%s'", qfpathname);
364 			quotause_destroy(qup);
365 			return NULL;
366 		}
367 		warnx("Creating quota file %s", qfpathname);
368 		sleep(3);
369 		(void)fchown(fd, getuid(),
370 		    getidbyname(quotagroup, QUOTA_CLASS_GROUP));
371 		(void)fchmod(fd, 0640);
372 	}
373 	(void)lseek(fd, (off_t)(id * sizeof(struct dqblk)),
374 	    SEEK_SET);
375 	switch (read(fd, &dqblk, sizeof(struct dqblk))) {
376 	case 0:			/* EOF */
377 		/*
378 		 * Convert implicit 0 quota (EOF)
379 		 * into an explicit one (zero'ed dqblk)
380 		 */
381 		memset(&dqblk, 0, sizeof(struct dqblk));
382 		break;
383 
384 	case sizeof(struct dqblk):	/* OK */
385 		break;
386 
387 	default:		/* ERROR */
388 		warn("read error in `%s'", qfpathname);
389 		close(fd);
390 		quotause_destroy(qup);
391 		return NULL;
392 	}
393 	close(fd);
394 	qup->qfname = qfpathname;
395 	endfsent();
396 	dqblk_to_quotavals(&dqblk,
397 			   &qup->qv[QUOTA_LIMIT_BLOCK],
398 			   &qup->qv[QUOTA_LIMIT_FILE]);
399 	return qup;
400 }
401 #endif
402 
403 ////////////////////////////////////////////////////////////
404 // generic quota interface
405 
406 static int
dogetprivs2(struct quotahandle * qh,int idtype,id_t id,int defaultq,int objtype,struct quotause * qup)407 dogetprivs2(struct quotahandle *qh, int idtype, id_t id, int defaultq,
408 	    int objtype, struct quotause *qup)
409 {
410 	struct quotakey qk;
411 
412 	qk.qk_idtype = idtype;
413 	qk.qk_id = defaultq ? QUOTA_DEFAULTID : id;
414 	qk.qk_objtype = objtype;
415 	if (quota_get(qh, &qk, &qup->qv[objtype]) == 0) {
416 		/* succeeded */
417 		qup->source[objtype] = SRC_QUOTA;
418 		return 0;
419 	}
420 	if (errno != ENOENT) {
421 		/* serious failure */
422 		return -1;
423 	}
424 
425 	/* no entry, get default entry */
426 	qk.qk_id = QUOTA_DEFAULTID;
427 	if (quota_get(qh, &qk, &qup->qv[objtype]) == 0) {
428 		/* succeeded */
429 		qup->source[objtype] = SRC_DEFAULT;
430 		return 0;
431 	}
432 	if (errno != ENOENT) {
433 		return -1;
434 	}
435 
436 	/* use a zeroed-out entry */
437 	quotaval_clear(&qup->qv[objtype]);
438 	qup->source[objtype] = SRC_CLEAR;
439 	return 0;
440 }
441 
442 static struct quotause *
getprivs2(long id,int idtype,const char * filesys,int defaultq,char ** idtypename_p)443 getprivs2(long id, int idtype, const char *filesys, int defaultq,
444 	  char **idtypename_p)
445 {
446 	struct quotause *qup;
447 	struct quotahandle *qh;
448 	const char *impl;
449 	unsigned restrictions;
450 	const char *idtypename;
451 	int serrno;
452 
453 	qup = quotause_create();
454 	strcpy(qup->fsname, filesys);
455 	if (defaultq)
456 		qup->isdefault = 1;
457 
458 	qh = quota_open(filesys);
459 	if (qh == NULL) {
460 		serrno = errno;
461 		quotause_destroy(qup);
462 		errno = serrno;
463 		return NULL;
464 	}
465 
466 	impl = quota_getimplname(qh);
467 	if (impl == NULL) {
468 		impl = "???";
469 	}
470 	strlcpy(qup->implementation, impl, sizeof(qup->implementation));
471 
472 	restrictions = quota_getrestrictions(qh);
473 	if ((restrictions & QUOTA_RESTRICT_UNIFORMGRACE) == 0) {
474 		qup->xgrace = 1;
475 	}
476 
477 	if (*idtypename_p == NULL) {
478 		idtypename = quota_idtype_getname(qh, idtype);
479 		*idtypename_p = strdup(idtypename);
480 		if (*idtypename_p == NULL) {
481 			errx(1, "Out of memory");
482 		}
483 	}
484 
485 	if (dogetprivs2(qh, idtype, id, defaultq, QUOTA_OBJTYPE_BLOCKS, qup)) {
486 		serrno = errno;
487 		quota_close(qh);
488 		quotause_destroy(qup);
489 		errno = serrno;
490 		return NULL;
491 	}
492 
493 	if (dogetprivs2(qh, idtype, id, defaultq, QUOTA_OBJTYPE_FILES, qup)) {
494 		serrno = errno;
495 		quota_close(qh);
496 		quotause_destroy(qup);
497 		errno = serrno;
498 		return NULL;
499 	}
500 
501 	quota_close(qh);
502 
503 	return qup;
504 }
505 
506 static void
putprivs2(uint32_t id,int idtype,struct quotause * qup)507 putprivs2(uint32_t id, int idtype, struct quotause *qup)
508 {
509 	struct quotahandle *qh;
510 	struct quotakey qk;
511 	char idname[32];
512 
513 	if (qup->isdefault) {
514 		snprintf(idname, sizeof(idname), "%s default",
515 			 idtype == QUOTA_IDTYPE_USER ? "user" : "group");
516 		id = QUOTA_DEFAULTID;
517 	} else {
518 		snprintf(idname, sizeof(idname), "%s %u",
519 			 idtype == QUOTA_IDTYPE_USER ? "uid" : "gid", id);
520 	}
521 
522 	qh = quota_open(qup->fsname);
523 	if (qh == NULL) {
524 		err(1, "%s: quota_open", qup->fsname);
525 	}
526 
527 	if (source_is_real(qup->source[QUOTA_OBJTYPE_BLOCKS])) {
528 		qk.qk_idtype = idtype;
529 		qk.qk_id = id;
530 		qk.qk_objtype = QUOTA_OBJTYPE_BLOCKS;
531 		if (quota_put(qh, &qk, &qup->qv[QO_BLK])) {
532 			err(1, "%s: quota_put (%s blocks)", qup->fsname,
533 			    idname);
534 		}
535 	}
536 
537 	if (source_is_real(qup->source[QUOTA_OBJTYPE_FILES])) {
538 		qk.qk_idtype = idtype;
539 		qk.qk_id = id;
540 		qk.qk_objtype = QUOTA_OBJTYPE_FILES;
541 		if (quota_put(qh, &qk, &qup->qv[QO_FL])) {
542 			err(1, "%s: quota_put (%s files)", qup->fsname,
543 			    idname);
544 		}
545 	}
546 
547 	quota_close(qh);
548 }
549 
550 ////////////////////////////////////////////////////////////
551 // quota format switch
552 
553 /*
554  * Collect the requested quota information.
555  */
556 static struct quotalist *
getprivs(long id,int defaultq,int idtype,const char * filesys)557 getprivs(long id, int defaultq, int idtype, const char *filesys)
558 {
559 	struct statvfs *fst;
560 	int nfst, i;
561 	struct quotalist *qlist;
562 	struct quotause *qup;
563 	int seenany = 0;
564 
565 	qlist = quotalist_create();
566 
567 	nfst = getmntinfo(&fst, MNT_WAIT);
568 	if (nfst == 0)
569 		errx(1, "no filesystems mounted!");
570 
571 	for (i = 0; i < nfst; i++) {
572 		if ((fst[i].f_flag & ST_QUOTA) == 0)
573 			continue;
574 		seenany = 1;
575 		if (filesys &&
576 		    strcmp(fst[i].f_mntonname, filesys) != 0 &&
577 		    strcmp(fst[i].f_mntfromname, filesys) != 0)
578 			continue;
579 		qup = getprivs2(id, idtype, fst[i].f_mntonname, defaultq,
580 				&qlist->idtypename);
581 		if (qup == NULL) {
582 			/* XXX the atf tests demand failing silently */
583 			/*warn("Reading quotas failed for id %ld", id);*/
584 			continue;
585 		}
586 
587 		quotalist_append(qlist, qup);
588 	}
589 
590 	if (!seenany) {
591 		errx(1, "No mounted filesystems have quota support");
592 	}
593 
594 #if 0
595 	if (filesys && quotalist_empty(qlist)) {
596 		if (defaultq)
597 			errx(1, "no default quota for version 1");
598 		/* if we get there, filesys is not mounted. try the old way */
599 		qup = getprivs1(id, idtype, filesys);
600 		if (qup == NULL) {
601 			warnx("getprivs1 failed");
602 			return qlist;
603 		}
604 		quotalist_append(qlist, qup);
605 	}
606 #endif
607 	return qlist;
608 }
609 
610 /*
611  * Store the requested quota information.
612  */
613 static void
putprivs(uint32_t id,int idtype,struct quotalist * qlist)614 putprivs(uint32_t id, int idtype, struct quotalist *qlist)
615 {
616 	struct quotause *qup;
617 
618         for (qup = qlist->head; qup; qup = qup->next) {
619 #if 0
620 		if (qup->qfname != NULL)
621 			putprivs1(id, idtype, qup);
622 		else
623 #endif
624 			putprivs2(id, idtype, qup);
625 	}
626 }
627 
628 static void
clearpriv(int argc,char ** argv,const char * filesys,int idtype)629 clearpriv(int argc, char **argv, const char *filesys, int idtype)
630 {
631 	struct statvfs *fst;
632 	int nfst, i;
633 	int id;
634 	id_t *ids;
635 	unsigned nids, maxids, j;
636 	struct quotahandle *qh;
637 	struct quotakey qk;
638 	char idname[32];
639 
640 	maxids = 4;
641 	nids = 0;
642 	ids = NULL;
643 	if (reallocarr(&ids, maxids, sizeof(ids[0])) != 0) {
644 		err(1, "reallocarr");
645 	}
646 
647 	for ( ; argc > 0; argc--, argv++) {
648 		if ((id = getidbyname(*argv, idtype)) == -1)
649 			continue;
650 
651 		if (nids + 1 > maxids) {
652 			maxids *= 2;
653 			if (reallocarr(&ids, maxids, sizeof(ids[0])) != 0) {
654 				err(1, "reallocarr");
655 			}
656 		}
657 		ids[nids++] = id;
658 	}
659 
660 	/* now loop over quota-enabled filesystems */
661 	nfst = getmntinfo(&fst, MNT_WAIT);
662 	if (nfst == 0)
663 		errx(1, "no filesystems mounted!");
664 
665 	for (i = 0; i < nfst; i++) {
666 		if ((fst[i].f_flag & ST_QUOTA) == 0)
667 			continue;
668 		if (filesys && strcmp(fst[i].f_mntonname, filesys) != 0 &&
669 		    strcmp(fst[i].f_mntfromname, filesys) != 0)
670 			continue;
671 
672 		qh = quota_open(fst[i].f_mntonname);
673 		if (qh == NULL) {
674 			err(1, "%s: quota_open", fst[i].f_mntonname);
675 		}
676 
677 		for (j = 0; j < nids; j++) {
678 			snprintf(idname, sizeof(idname), "%s %u",
679 				 idtype == QUOTA_IDTYPE_USER ?
680 				 "uid" : "gid", ids[j]);
681 
682 			qk.qk_idtype = idtype;
683 			qk.qk_id = ids[j];
684 			qk.qk_objtype = QUOTA_OBJTYPE_BLOCKS;
685 			if (quota_delete(qh, &qk)) {
686 				err(1, "%s: quota_delete (%s blocks)",
687 				    fst[i].f_mntonname, idname);
688 			}
689 
690 			qk.qk_idtype = idtype;
691 			qk.qk_id = ids[j];
692 			qk.qk_objtype = QUOTA_OBJTYPE_FILES;
693 			if (quota_delete(qh, &qk)) {
694 				if (errno == ENOENT) {
695  					/*
696 					 * XXX ignore this case; due
697 					 * to a weakness in the quota
698 					 * proplib interface it can
699 					 * appear spuriously.
700 					 */
701 				} else {
702 					err(1, "%s: quota_delete (%s files)",
703 					    fst[i].f_mntonname, idname);
704 				}
705 			}
706 		}
707 
708 		quota_close(qh);
709 	}
710 
711 	free(ids);
712 }
713 
714 ////////////////////////////////////////////////////////////
715 // editor
716 
717 /*
718  * Take a list of privileges and get it edited.
719  */
720 static int
editit(const char * ltmpfile)721 editit(const char *ltmpfile)
722 {
723 	pid_t pid;
724 	int lst;
725 	char p[MAX_TMPSTR];
726 	const char *ed;
727 	sigset_t s, os;
728 
729 	sigemptyset(&s);
730 	sigaddset(&s, SIGINT);
731 	sigaddset(&s, SIGQUIT);
732 	sigaddset(&s, SIGHUP);
733 	if (sigprocmask(SIG_BLOCK, &s, &os) == -1)
734 		err(1, "sigprocmask");
735 top:
736 	switch ((pid = fork())) {
737 	case -1:
738 		if (errno == EPROCLIM) {
739 			warnx("You have too many processes");
740 			return 0;
741 		}
742 		if (errno == EAGAIN) {
743 			sleep(1);
744 			goto top;
745 		}
746 		warn("fork");
747 		return 0;
748 	case 0:
749 		if (sigprocmask(SIG_SETMASK, &os, NULL) == -1)
750 			err(1, "sigprocmask");
751 		setgid(getgid());
752 		setuid(getuid());
753 		if ((ed = getenv("EDITOR")) == (char *)0)
754 			ed = _PATH_VI;
755 		if ((size_t)snprintf(p, sizeof(p), "%s %s", ed, ltmpfile) >=
756 		    sizeof(p)) {
757 			errx(1, "%s", "editor or filename too long");
758 		}
759 		execlp(_PATH_BSHELL, _PATH_BSHELL, "-c", p, NULL);
760 		err(1, "%s", ed);
761 	default:
762 		if (waitpid(pid, &lst, 0) == -1)
763 			err(1, "waitpid");
764 		if (sigprocmask(SIG_SETMASK, &os, NULL) == -1)
765 			err(1, "sigprocmask");
766 		if (!WIFEXITED(lst) || WEXITSTATUS(lst) != 0)
767 			return 0;
768 		return 1;
769 	}
770 }
771 
772 /*
773  * Convert a quotause list to an ASCII file.
774  */
775 static int
writeprivs(struct quotalist * qlist,int outfd,const char * name,int idtype,const char * idtypename)776 writeprivs(struct quotalist *qlist, int outfd, const char *name,
777     int idtype, const char *idtypename)
778 {
779 	struct quotause *qup;
780 	FILE *fd;
781 	char b0[32], b1[32], b2[32], b3[32];
782 	const char *comm;
783 
784 	(void)ftruncate(outfd, 0);
785 	(void)lseek(outfd, (off_t)0, SEEK_SET);
786 	if ((fd = fdopen(dup(outfd), "w")) == NULL)
787 		errx(1, "fdopen");
788 	if (name == NULL) {
789 		fprintf(fd, "Default %s quotas:\n", idtypename);
790 	} else {
791 		fprintf(fd, "Quotas for %s %s:\n", idtypename, name);
792 	}
793 	for (qup = qlist->head; qup; qup = qup->next) {
794 		struct quotaval *q = qup->qv;
795 
796 		fprintf(fd, "%s (%s):\n", qup->fsname, qup->implementation);
797 
798 		comm = source_is_real(qup->source[QO_BLK]) ? "" : "#";
799 		fprintf(fd, "\tblocks:%s\n",
800 			Hflag ? "" : " (sizes in 1K-blocks)");
801 		fprintf(fd, "\t\t%susage: %s\n", comm,
802 			intprt(b1, 21, q[QO_BLK].qv_usage,
803 			       HN_NOSPACE | HN_B, Hflag));
804 		fprintf(fd, "\t\t%slimits: soft %s, hard %s\n", comm,
805 			intprt(b2, 21, q[QO_BLK].qv_softlimit,
806 			       HN_NOSPACE | HN_B, Hflag),
807 			intprt(b3, 21, q[QO_BLK].qv_hardlimit,
808 			       HN_NOSPACE | HN_B, Hflag));
809 		if (qup->xgrace || qup->isdefault) {
810 			fprintf(fd, "\t\t%sgrace: %s\n", comm,
811 				timepprt(b0, 21, q[QO_BLK].qv_grace, Hflag));
812 		}
813 
814 		comm = source_is_real(qup->source[QO_FL]) ? "" : "#";
815 		fprintf(fd, "\tinodes:\n");
816 		fprintf(fd, "\t\t%susage: %s\n", comm,
817 			intprt(b1, 21, q[QO_FL].qv_usage,
818 			       HN_NOSPACE, Hflag));
819 		fprintf(fd, "\t\t%slimits: soft %s, hard %s\n", comm,
820 			intprt(b2, 21, q[QO_FL].qv_softlimit,
821 			       HN_NOSPACE, Hflag),
822 			intprt(b3, 21, q[QO_FL].qv_hardlimit,
823 			       HN_NOSPACE, Hflag));
824 		if (qup->xgrace || qup->isdefault) {
825 			fprintf(fd, "\t\t%sgrace: %s\n", comm,
826 				timepprt(b0, 21, q[QO_FL].qv_grace, Hflag));
827 		}
828 	}
829 	fclose(fd);
830 	return 1;
831 }
832 
833 /*
834  * Merge changes to an ASCII file into a quotause list.
835  */
836 static int
readprivs(struct quotalist * qlist,int infd,int dflag)837 readprivs(struct quotalist *qlist, int infd, int dflag)
838 {
839 	FILE *fd;			/* file */
840 	unsigned lineno;		/* line number in file */
841 	char line[BUFSIZ];		/* input buffer */
842 	size_t len;			/* length of input buffer */
843 	bool seenheader;		/* true if past the file header */
844 	struct quotause *qup;		/* current filesystem */
845 	bool haveobjtype;		/* true if objtype is valid */
846 	unsigned objtype;		/* current object type */
847 	int objtypeflags;		/* humanize flags */
848 	/* XXX make following const later (requires non-local cleanup) */
849 	/*const*/ char *text, *text2, *t; /* scratch values */
850 	char b0[32], b1[32];
851 	uint64_t soft, hard, current;
852 	time_t grace;
853 	struct quotaval *qv;
854 
855 	lineno = 0;
856 	seenheader = false;
857 	qup = NULL;
858 	haveobjtype = false;
859 	objtype = QUOTA_OBJTYPE_BLOCKS; /* for gcc 4.5 */
860 	objtypeflags = HN_B;
861 
862 	(void)lseek(infd, (off_t)0, SEEK_SET);
863 	fd = fdopen(dup(infd), "r");
864 	if (fd == NULL) {
865 		warn("Can't re-read temp file");
866 		return -1;
867 	}
868 
869 	/*
870 	 * Read back the same format we wrote.
871 	 */
872 
873 	while (fgets(line, sizeof(line), fd)) {
874 		lineno++;
875 		if (!seenheader) {
876 			if ((!strncmp(line, "Default ", 8) && dflag) ||
877 			    (!strncmp(line, "Quotas for ", 11) && !dflag)) {
878 				/* ok. */
879 				seenheader = 1;
880 				continue;
881 			}
882 			warnx("Header line missing");
883 			goto fail;
884 		}
885 		len = strlen(line);
886 		if (len == 0) {
887 			/* ? */
888 			continue;
889 		}
890 		if (line[len - 1] != '\n') {
891 			warnx("Line %u too long", lineno);
892 			goto fail;
893 		}
894 		line[--len] = '\0';
895 		if (line[len - 1] == '\r') {
896 			line[--len] = '\0';
897 		}
898 		if (len == 0) {
899 			/* blank line */
900 			continue;
901 		}
902 
903 		/*
904 		 * If the line has:     it is:
905                  *      two tabs        values
906 		 *      one tab         the next objtype
907 		 *      no tabs         the next filesystem
908 		 */
909 		if (line[0] == '\t' && line[1] == '\t') {
910 			if (qup == NULL) {
911 				warnx("Line %u: values with no filesystem",
912 				      lineno);
913 				goto fail;
914 			}
915 			if (!haveobjtype) {
916 				warnx("Line %u: values with no object type",
917 				      lineno);
918 				goto fail;
919 			}
920 			qv = &qup->qv[objtype];
921 
922 			text = line + 2;
923 			if (*text == '#') {
924 				/* commented out; ignore */
925 				continue;
926 			}
927 			else if (!strncmp(text, "usage:", 6)) {
928 
929 				/* usage: %llu */
930 				text += 6;
931 				t = skipws(text);
932 				if (intrd(t, &current, objtypeflags) != 0) {
933 					warnx("Line %u: Bad number %s",
934 					      lineno, t);
935 					goto fail;
936 				}
937 
938 				/*
939 				 * Because the humanization can lead
940 				 * to roundoff, check if the two
941 				 * values produce the same humanized
942 				 * string, rather than if they're the
943 				 * same number. Sigh.
944 				 */
945 				intprt(b0, 21, current,
946 				       HN_NOSPACE | objtypeflags, Hflag);
947 				intprt(b1, 21, qv->qv_usage,
948 				       HN_NOSPACE | objtypeflags, Hflag);
949 				if (strcmp(b0, b1)) {
950 					warnx("Line %u: cannot change usage",
951 					      lineno);
952 				}
953 				continue;
954 
955 			} else if (!strncmp(text, "limits:", 7)) {
956 
957 				/* limits: soft %llu, hard %llu */
958 				text += 7;
959 				text2 = strchr(text, ',');
960 				if (text2 == NULL) {
961 					warnx("Line %u: expected comma",
962 					      lineno);
963 					goto fail;
964 				}
965 				*text2 = '\0';
966 				text2++;
967 
968 				t = skipws(text);
969 				t = skipword(t);
970 				t = skipws(t);
971 				if (intrd(t, &soft, objtypeflags) != 0) {
972 					warnx("Line %u: Bad number %s",
973 					      lineno, t);
974 					goto fail;
975 				}
976 				t = skipws(text2);
977 				t = skipword(t);
978 				t = skipws(t);
979 				if (intrd(t, &hard, objtypeflags) != 0) {
980 					warnx("Line %u: Bad number %s",
981 					      lineno, t);
982 					goto fail;
983 				}
984 
985 				/*
986 				 * Cause time limit to be reset when the quota
987 				 * is next used if previously had no soft limit
988 				 * or were under it, but now have a soft limit
989 				 * and are over it.
990 				 */
991 				if (qv->qv_usage && qv->qv_usage >= soft &&
992 				    (qv->qv_softlimit == 0 ||
993 				     qv->qv_usage < qv->qv_softlimit)) {
994 					qv->qv_expiretime = 0;
995 				}
996 				if (soft != qv->qv_softlimit ||
997 				    hard != qv->qv_hardlimit) {
998 					qv->qv_softlimit = soft;
999 					qv->qv_hardlimit = hard;
1000 					qup->source[objtype] = SRC_EDITED;
1001 				}
1002 				qup->found = 1;
1003 
1004 			} else if (!strncmp(text, "grace:", 6)) {
1005 
1006 				text += 6;
1007 				/* grace: %llu */
1008 				t = skipws(text);
1009 				if (timeprd(t, &grace) != 0) {
1010 					warnx("Line %u: Bad number %s",
1011 					      lineno, t);
1012 					goto fail;
1013 				}
1014 				if (qup->isdefault || qup->xgrace) {
1015 					if (grace != qv->qv_grace) {
1016 						qv->qv_grace = grace;
1017 						qup->source[objtype] =
1018 							SRC_EDITED;
1019 					}
1020 					qup->found = 1;
1021 				} else {
1022 					warnx("Line %u: Cannot set individual "
1023 					      "grace time on this filesystem",
1024 					      lineno);
1025 					goto fail;
1026 				}
1027 
1028 			} else {
1029 				warnx("Line %u: Unknown/unexpected value line",
1030 				      lineno);
1031 				goto fail;
1032 			}
1033 		} else if (line[0] == '\t') {
1034 			text = line + 1;
1035 			if (*text == '#') {
1036 				/* commented out; ignore */
1037 				continue;
1038 			}
1039 			else if (!strncmp(text, "blocks:", 7)) {
1040 				objtype = QUOTA_OBJTYPE_BLOCKS;
1041 				objtypeflags = HN_B;
1042 				haveobjtype = true;
1043 			} else if (!strncmp(text, "inodes:", 7)) {
1044 				objtype = QUOTA_OBJTYPE_FILES;
1045 				objtypeflags = 0;
1046 				haveobjtype = true;
1047 			} else {
1048 				warnx("Line %u: Unknown/unexpected object "
1049 				      "type (%s)", lineno, text);
1050 				goto fail;
1051 			}
1052 		} else {
1053 			t = strchr(line, ' ');
1054 			if (t == NULL) {
1055 				t = strchr(line, ':');
1056 				if (t == NULL) {
1057 					t = line + len;
1058 				}
1059 			}
1060 			*t = '\0';
1061 
1062 			if (*line == '#') {
1063 				/* commented out; ignore */
1064 				continue;
1065 			}
1066 
1067 			for (qup = qlist->head; qup; qup = qup->next) {
1068 				if (!strcmp(line, qup->fsname))
1069 					break;
1070 			}
1071 			if (qup == NULL) {
1072 				warnx("Line %u: Filesystem %s invalid or has "
1073 				      "no quota support", lineno, line);
1074 				goto fail;
1075 			}
1076 			haveobjtype = false;
1077 		}
1078 	}
1079 
1080 	fclose(fd);
1081 
1082 	/*
1083 	 * Disable quotas for any filesystems that we didn't see,
1084 	 * because they must have been deleted in the editor.
1085 	 *
1086 	 * XXX this should be improved so it results in
1087 	 * quota_delete(), not just writing out a blank quotaval.
1088 	 */
1089 	for (qup = qlist->head; qup; qup = qup->next) {
1090 		if (qup->found) {
1091 			qup->found = 0;
1092 			continue;
1093 		}
1094 
1095 		if (source_is_real(qup->source[QUOTA_OBJTYPE_BLOCKS])) {
1096 			quotaval_clear(&qup->qv[QUOTA_OBJTYPE_BLOCKS]);
1097 			qup->source[QUOTA_OBJTYPE_BLOCKS] = SRC_EDITED;
1098 		}
1099 
1100 		if (source_is_real(qup->source[QUOTA_OBJTYPE_FILES])) {
1101 			quotaval_clear(&qup->qv[QUOTA_OBJTYPE_FILES]);
1102 			qup->source[QUOTA_OBJTYPE_FILES] = SRC_EDITED;
1103 		}
1104 	}
1105 	return 0;
1106 
1107 fail:
1108 	sleep(3);
1109 	fclose(fd);
1110 	return -1;
1111 }
1112 
1113 ////////////////////////////////////////////////////////////
1114 // actions
1115 
1116 static void
replicate(const char * fs,int idtype,const char * protoname,char ** names,int numnames)1117 replicate(const char *fs, int idtype, const char *protoname,
1118 	  char **names, int numnames)
1119 {
1120 	long protoid, id;
1121 	struct quotalist *protoprivs;
1122 	struct quotause *qup;
1123 	int i;
1124 
1125 	if ((protoid = getidbyname(protoname, idtype)) == -1)
1126 		exit(1);
1127 	protoprivs = getprivs(protoid, 0, idtype, fs);
1128 	for (qup = protoprivs->head; qup; qup = qup->next) {
1129 		qup->qv[QO_BLK].qv_expiretime = 0;
1130 		qup->qv[QO_FL].qv_expiretime = 0;
1131 		qup->source[QO_BLK] = SRC_EDITED;
1132 		qup->source[QO_FL] = SRC_EDITED;
1133 	}
1134 	for (i=0; i<numnames; i++) {
1135 		id = getidbyname(names[i], idtype);
1136 		if (id == -1)
1137 			continue;
1138 		putprivs(id, idtype, protoprivs);
1139 	}
1140 	/* XXX */
1141 	/* quotalist_destroy(protoprivs); */
1142 }
1143 
1144 static void
assign(const char * fs,int idtype,char * soft,char * hard,char * grace,char ** names,int numnames)1145 assign(const char *fs, int idtype,
1146        char *soft, char *hard, char *grace,
1147        char **names, int numnames)
1148 {
1149 	struct quotalist *curprivs;
1150 	struct quotause *lqup;
1151 	u_int64_t softb, hardb, softi, hardi;
1152 	time_t  graceb, gracei;
1153 	char *str;
1154 	long id;
1155 	int dflag;
1156 	int i;
1157 
1158 	if (soft) {
1159 		str = strsep(&soft, "/");
1160 		if (str[0] == '\0' || soft == NULL || soft[0] == '\0')
1161 			usage();
1162 
1163 		if (intrd(str, &softb, HN_B) != 0)
1164 			errx(1, "%s: bad number", str);
1165 		if (intrd(soft, &softi, 0) != 0)
1166 			errx(1, "%s: bad number", soft);
1167 	}
1168 	if (hard) {
1169 		str = strsep(&hard, "/");
1170 		if (str[0] == '\0' || hard == NULL || hard[0] == '\0')
1171 			usage();
1172 
1173 		if (intrd(str, &hardb, HN_B) != 0)
1174 			errx(1, "%s: bad number", str);
1175 		if (intrd(hard, &hardi, 0) != 0)
1176 			errx(1, "%s: bad number", hard);
1177 	}
1178 	if (grace) {
1179 		str = strsep(&grace, "/");
1180 		if (str[0] == '\0' || grace == NULL || grace[0] == '\0')
1181 			usage();
1182 
1183 		if (timeprd(str, &graceb) != 0)
1184 			errx(1, "%s: bad number", str);
1185 		if (timeprd(grace, &gracei) != 0)
1186 			errx(1, "%s: bad number", grace);
1187 	}
1188 	for (i=0; i<numnames; i++) {
1189 		if (names[i] == NULL) {
1190 			id = 0;
1191 			dflag = 1;
1192 		} else {
1193 			id = getidbyname(names[i], idtype);
1194 			if (id == -1)
1195 				continue;
1196 			dflag = 0;
1197 		}
1198 
1199 		curprivs = getprivs(id, dflag, idtype, fs);
1200 		for (lqup = curprivs->head; lqup; lqup = lqup->next) {
1201 			struct quotaval *q = lqup->qv;
1202 			if (soft) {
1203 				if (!dflag && softb &&
1204 				    q[QO_BLK].qv_usage >= softb &&
1205 				    (q[QO_BLK].qv_softlimit == 0 ||
1206 				     q[QO_BLK].qv_usage <
1207 				     q[QO_BLK].qv_softlimit))
1208 					q[QO_BLK].qv_expiretime = 0;
1209 				if (!dflag && softi &&
1210 				    q[QO_FL].qv_usage >= softb &&
1211 				    (q[QO_FL].qv_softlimit == 0 ||
1212 				     q[QO_FL].qv_usage <
1213 				     q[QO_FL].qv_softlimit))
1214 					q[QO_FL].qv_expiretime = 0;
1215 				q[QO_BLK].qv_softlimit = softb;
1216 				q[QO_FL].qv_softlimit = softi;
1217 			}
1218 			if (hard) {
1219 				q[QO_BLK].qv_hardlimit = hardb;
1220 				q[QO_FL].qv_hardlimit = hardi;
1221 			}
1222 			if (grace) {
1223 				q[QO_BLK].qv_grace = graceb;
1224 				q[QO_FL].qv_grace = gracei;
1225 			}
1226 			lqup->source[QO_BLK] = SRC_EDITED;
1227 			lqup->source[QO_FL] = SRC_EDITED;
1228 		}
1229 		putprivs(id, idtype, curprivs);
1230 		quotalist_destroy(curprivs);
1231 	}
1232 }
1233 
1234 static void
clear(const char * fs,int idtype,char ** names,int numnames)1235 clear(const char *fs, int idtype, char **names, int numnames)
1236 {
1237 	clearpriv(numnames, names, fs, idtype);
1238 }
1239 
1240 static void
editone(const char * fs,int idtype,const char * name,int tmpfd,const char * tmppath)1241 editone(const char *fs, int idtype, const char *name,
1242 	int tmpfd, const char *tmppath)
1243 {
1244 	struct quotalist *curprivs;
1245 	long id;
1246 	int dflag;
1247 
1248 	if (name == NULL) {
1249 		id = 0;
1250 		dflag = 1;
1251 	} else {
1252 		id = getidbyname(name, idtype);
1253 		if (id == -1)
1254 			return;
1255 		dflag = 0;
1256 	}
1257 	curprivs = getprivs(id, dflag, idtype, fs);
1258 
1259 	if (writeprivs(curprivs, tmpfd, name, idtype,
1260 		       curprivs->idtypename) == 0)
1261 		goto fail;
1262 
1263 	if (editit(tmppath) == 0)
1264 		goto fail;
1265 
1266 	if (readprivs(curprivs, tmpfd, dflag) < 0)
1267 		goto fail;
1268 
1269 	putprivs(id, idtype, curprivs);
1270 fail:
1271 	quotalist_destroy(curprivs);
1272 }
1273 
1274 static void
edit(const char * fs,int idtype,char ** names,int numnames)1275 edit(const char *fs, int idtype, char **names, int numnames)
1276 {
1277 	char tmppath[] = _PATH_TMPFILE;
1278 	int tmpfd, i;
1279 
1280 	tmpfd = mkstemp(tmppath);
1281 	fchown(tmpfd, getuid(), getgid());
1282 
1283 	for (i=0; i<numnames; i++) {
1284 		editone(fs, idtype, names[i], tmpfd, tmppath);
1285 	}
1286 
1287 	close(tmpfd);
1288 	unlink(tmppath);
1289 }
1290 
1291 ////////////////////////////////////////////////////////////
1292 // main
1293 
1294 static void
usage(void)1295 usage(void)
1296 {
1297 	const char *p = getprogname();
1298 	fprintf(stderr,
1299 	    "Usage: %s [-D] [-H] [-u] [-p <username>] [-f <filesystem>] "
1300 		"-d | <username> ...\n"
1301 	    "\t%s [-D] [-H] -g [-p <groupname>] [-f <filesystem>] "
1302 		"-d | <groupname> ...\n"
1303 	    "\t%s [-D] [-u] [-f <filesystem>] [-s b#/i#] [-h b#/i#] [-t t#/t#] "
1304 		"-d | <username> ...\n"
1305 	    "\t%s [-D] -g [-f <filesystem>] [-s b#/i#] [-h b#/i#] [-t t#/t#] "
1306 		"-d | <groupname> ...\n"
1307 	    "\t%s [-D] [-H] [-u] -c [-f <filesystem>] username ...\n"
1308 	    "\t%s [-D] [-H] -g -c [-f <filesystem>] groupname ...\n",
1309 	    p, p, p, p, p, p);
1310 	exit(1);
1311 }
1312 
1313 int
main(int argc,char * argv[])1314 main(int argc, char *argv[])
1315 {
1316 	int idtype;
1317 	char *protoname;
1318 	char *soft = NULL, *hard = NULL, *grace = NULL;
1319 	char *fs = NULL;
1320 	int ch;
1321 	int pflag = 0;
1322 	int cflag = 0;
1323 	int dflag = 0;
1324 
1325 	if (argc < 2)
1326 		usage();
1327 	if (getuid())
1328 		errx(1, "permission denied");
1329 	protoname = NULL;
1330 	idtype = QUOTA_IDTYPE_USER;
1331 	while ((ch = getopt(argc, argv, "Hcdugp:s:h:t:f:")) != -1) {
1332 		switch(ch) {
1333 		case 'H':
1334 			Hflag++;
1335 			break;
1336 		case 'c':
1337 			cflag++;
1338 			break;
1339 		case 'd':
1340 			dflag++;
1341 			break;
1342 		case 'p':
1343 			protoname = optarg;
1344 			pflag++;
1345 			break;
1346 		case 'g':
1347 			idtype = QUOTA_IDTYPE_GROUP;
1348 			break;
1349 		case 'u':
1350 			idtype = QUOTA_IDTYPE_USER;
1351 			break;
1352 		case 's':
1353 			soft = optarg;
1354 			break;
1355 		case 'h':
1356 			hard = optarg;
1357 			break;
1358 		case 't':
1359 			grace = optarg;
1360 			break;
1361 		case 'f':
1362 			fs = optarg;
1363 			break;
1364 		default:
1365 			usage();
1366 		}
1367 	}
1368 	argc -= optind;
1369 	argv += optind;
1370 
1371 	if (pflag) {
1372 		if (soft || hard || grace || dflag || cflag)
1373 			usage();
1374 		replicate(fs, idtype, protoname, argv, argc);
1375 	} else if (soft || hard || grace) {
1376 		if (cflag)
1377 			usage();
1378 		if (dflag) {
1379 			/* use argv[argc], which is null, to mean 'default' */
1380 			argc++;
1381 		}
1382 		assign(fs, idtype, soft, hard, grace, argv, argc);
1383 	} else if (cflag) {
1384 		if (dflag)
1385 			usage();
1386 		clear(fs, idtype, argv, argc);
1387 	} else {
1388 		if (dflag) {
1389 			/* use argv[argc], which is null, to mean 'default' */
1390 			argc++;
1391 		}
1392 		edit(fs, idtype, argv, argc);
1393 	}
1394 	return 0;
1395 }
1396