1 /*
2 * CDDL HEADER START
3 *
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License (the "License").
6 * You may not use this file except in compliance with the License.
7 *
8 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9 * or http://www.opensolaris.org/os/licensing.
10 * See the License for the specific language governing permissions
11 * and limitations under the License.
12 *
13 * When distributing Covered Code, include this CDDL HEADER in each
14 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15 * If applicable, add the following below this CDDL HEADER, with the
16 * fields enclosed by brackets "[]" replaced with your own identifying
17 * information: Portions Copyright [yyyy] [name of copyright owner]
18 *
19 * CDDL HEADER END
20 */
21 /*
22 * Copyright 2009 Sun Microsystems, Inc. All rights reserved.
23 * Use is subject to license terms.
24 */
25
26 #include <sys/types.h>
27 #include <sys/errno.h>
28 #include <sys/tiuser.h>
29 #include <setjmp.h>
30 #include <pwd.h>
31 #include <grp.h>
32
33 #include <rpc/types.h>
34 #include <rpc/xdr.h>
35 #include <rpc/auth.h>
36 #include <rpc/clnt.h>
37 #include <rpc/rpc_msg.h>
38 #include <string.h>
39 #include "snoop.h"
40
41 #include <sys/stat.h>
42
43 extern char *get_sum_line();
44 extern void check_retransmit();
45 extern char *sum_nfsfh();
46 extern int sum_nfsstat();
47 extern int detail_nfsstat();
48 extern void detail_nfsfh();
49 extern void detail_fattr();
50 extern void skip_fattr();
51 extern char *sum_nfsfh3();
52 extern int sum_nfsstat3();
53 extern int detail_nfsstat3();
54 extern void detail_post_op_attr();
55 extern void detail_nfsfh3();
56 extern int sum_nfsstat4();
57 extern int detail_nfsstat4();
58
59 extern jmp_buf xdr_err;
60
61 static void aclcall2();
62 static void aclreply2();
63 static void aclcall3();
64 static void aclreply3();
65 static void aclcall4();
66 static void aclreply4();
67 static void detail_access2();
68 static char *sum_access2();
69 static void detail_mask();
70 static void detail_secattr();
71 static void detail_aclent();
72 static char *detail_uname();
73 static char *detail_gname();
74 static char *detail_perm(ushort_t);
75 static void interpret_nfs_acl2(int, int, int, int, int, char *, int);
76 static void interpret_nfs_acl3(int, int, int, int, int, char *, int);
77 static void interpret_nfs_acl4(int, int, int, int, int, char *, int);
78
79 #define ACLPROC2_NULL ((unsigned long)(0))
80 #define ACLPROC2_GETACL ((unsigned long)(1))
81 #define ACLPROC2_SETACL ((unsigned long)(2))
82 #define ACLPROC2_GETATTR ((unsigned long)(3))
83 #define ACLPROC2_ACCESS ((unsigned long)(4))
84 #define ACLPROC2_GETXATTRDIR ((unsigned long)(5))
85
86 #define ACLPROC3_NULL ((unsigned long)(0))
87 #define ACLPROC3_GETACL ((unsigned long)(1))
88 #define ACLPROC3_SETACL ((unsigned long)(2))
89 #define ACLPROC3_GETXATTRDIR ((unsigned long)(3))
90
91 #define ACLPROC4_NULL ((unsigned long)(0))
92 #define ACLPROC4_GETACL ((unsigned long)(1))
93 #define ACLPROC4_SETACL ((unsigned long)(2))
94
95 #define NA_USER_OBJ 0x1
96 #define NA_USER 0x2
97 #define NA_GROUP_OBJ 0x4
98 #define NA_GROUP 0x8
99 #define NA_CLASS_OBJ 0x10
100 #define NA_OTHER_OBJ 0x20
101 #define NA_ACL_DEFAULT 0x1000
102
103 #define NA_DEF_USER_OBJ (NA_USER_OBJ | NA_ACL_DEFAULT)
104 #define NA_DEF_USER (NA_USER | NA_ACL_DEFAULT)
105 #define NA_DEF_GROUP_OBJ (NA_GROUP_OBJ | NA_ACL_DEFAULT)
106 #define NA_DEF_GROUP (NA_GROUP | NA_ACL_DEFAULT)
107 #define NA_DEF_CLASS_OBJ (NA_CLASS_OBJ | NA_ACL_DEFAULT)
108 #define NA_DEF_OTHER_OBJ (NA_OTHER_OBJ | NA_ACL_DEFAULT)
109
110 #define NA_ACL 0x1
111 #define NA_ACLCNT 0x2
112 #define NA_DFACL 0x4
113 #define NA_DFACLCNT 0x8
114
115 #define ACCESS2_READ 0x0001
116 #define ACCESS2_LOOKUP 0x0002
117 #define ACCESS2_MODIFY 0x0004
118 #define ACCESS2_EXTEND 0x0008
119 #define ACCESS2_DELETE 0x0010
120 #define ACCESS2_EXECUTE 0x0020
121
122 static char *procnames_short_v2[] = {
123 "NULL2", /* 0 */
124 "GETACL2", /* 1 */
125 "SETACL2", /* 2 */
126 "GETATTR2", /* 3 */
127 "ACCESS2", /* 4 */
128 "GETXATTRDIR2", /* 5 */
129 };
130 static char *procnames_short_v3[] = {
131 "NULL3", /* 0 */
132 "GETACL3", /* 1 */
133 "SETACL3", /* 2 */
134 "GETXATTRDIR3", /* 3 */
135 };
136 static char *procnames_short_v4[] = {
137 "NULL4", /* 0 */
138 "GETACL4", /* 1 */
139 "SETACL4", /* 2 */
140 };
141
142 static char *procnames_long_v2[] = {
143 "Null procedure", /* 0 */
144 "Get file access control list", /* 1 */
145 "Set file access control list", /* 2 */
146 "Get file attributes", /* 3 */
147 "Check access permission", /* 4 */
148 "Get extended attribute directory", /* 5 */
149 };
150 static char *procnames_long_v3[] = {
151 "Null procedure", /* 0 */
152 "Get file access control list", /* 1 */
153 "Set file access control list", /* 2 */
154 "Get extended attribute directory", /* 3 */
155 };
156 static char *procnames_long_v4[] = {
157 "Null procedure", /* 0 */
158 "Get file access control list", /* 1 */
159 "Set file access control list", /* 2 */
160 };
161
162 #define MAXPROC_V2 5
163 #define MAXPROC_V3 3
164 #define MAXPROC_V4 2
165
166 /* ARGSUSED */
167 void
interpret_nfs_acl(flags,type,xid,vers,proc,data,len)168 interpret_nfs_acl(flags, type, xid, vers, proc, data, len)
169 int flags, type, xid, vers, proc;
170 char *data;
171 int len;
172 {
173
174 if (vers == 2) {
175 interpret_nfs_acl2(flags, type, xid, vers, proc, data, len);
176 return;
177 }
178
179 if (vers == 3) {
180 interpret_nfs_acl3(flags, type, xid, vers, proc, data, len);
181 return;
182 }
183
184 if (vers == 4) {
185 interpret_nfs_acl4(flags, type, xid, vers, proc, data, len);
186 return;
187 }
188 }
189
190 static void
interpret_nfs_acl2(int flags,int type,int xid,int vers,int proc,char * data,int len)191 interpret_nfs_acl2(int flags, int type, int xid, int vers, int proc,
192 char *data, int len)
193 {
194 char *line;
195 char buff[2048];
196 int off, sz;
197 char *fh;
198 ulong_t mask;
199
200 if (proc < 0 || proc > MAXPROC_V2)
201 return;
202
203 if (flags & F_SUM) {
204 line = get_sum_line();
205
206 if (type == CALL) {
207 (void) sprintf(line, "NFS_ACL C %s",
208 procnames_short_v2[proc]);
209 line += strlen(line);
210 switch (proc) {
211 case ACLPROC2_GETACL:
212 fh = sum_nfsfh();
213 mask = getxdr_u_long();
214 (void) sprintf(line, "%s mask=%lu", fh, mask);
215 break;
216 case ACLPROC2_SETACL:
217 (void) sprintf(line, sum_nfsfh());
218 break;
219 case ACLPROC2_GETATTR:
220 (void) sprintf(line, sum_nfsfh());
221 break;
222 case ACLPROC2_ACCESS:
223 fh = sum_nfsfh();
224 (void) sprintf(line, "%s (%s)", fh,
225 sum_access2());
226 break;
227 case ACLPROC2_GETXATTRDIR:
228 fh = sum_nfsfh();
229 (void) sprintf(line, "%s create=%s", fh,
230 getxdr_bool() ? "true" : "false");
231 break;
232 default:
233 break;
234 }
235
236 check_retransmit(line, (ulong_t)xid);
237 } else {
238 (void) sprintf(line, "NFS_ACL R %s ",
239 procnames_short_v2[proc]);
240 line += strlen(line);
241 switch (proc) {
242 case ACLPROC2_GETACL:
243 (void) sum_nfsstat(line);
244 break;
245 case ACLPROC2_SETACL:
246 (void) sum_nfsstat(line);
247 break;
248 case ACLPROC2_GETATTR:
249 (void) sum_nfsstat(line);
250 break;
251 case ACLPROC2_ACCESS:
252 if (sum_nfsstat(line) == 0) {
253 skip_fattr();
254 line += strlen(line);
255 (void) sprintf(line, " (%s)",
256 sum_access2());
257 }
258 break;
259 case ACLPROC2_GETXATTRDIR:
260 if (sum_nfsstat(line) == 0) {
261 line += strlen(line);
262 (void) sprintf(line, sum_nfsfh());
263 }
264 break;
265 default:
266 break;
267 }
268 }
269 }
270
271 if (flags & F_DTAIL) {
272 show_header("NFS_ACL: ", "Sun NFS_ACL", len);
273 show_space();
274 (void) sprintf(get_line(0, 0), "Proc = %d (%s)",
275 proc, procnames_long_v2[proc]);
276 if (type == CALL)
277 aclcall2(proc);
278 else
279 aclreply2(proc);
280 show_trailer();
281 }
282 }
283
284 static void
interpret_nfs_acl3(int flags,int type,int xid,int vers,int proc,char * data,int len)285 interpret_nfs_acl3(int flags, int type, int xid, int vers, int proc,
286 char *data, int len)
287 {
288 char *line;
289 char buff[2048];
290 int off, sz;
291 char *fh;
292 ulong_t mask;
293
294 if (proc < 0 || proc > MAXPROC_V3)
295 return;
296
297 if (flags & F_SUM) {
298 line = get_sum_line();
299
300 if (type == CALL) {
301 (void) sprintf(line, "NFS_ACL C %s",
302 procnames_short_v3[proc]);
303 line += strlen(line);
304 switch (proc) {
305 case ACLPROC3_GETACL:
306 fh = sum_nfsfh3();
307 mask = getxdr_u_long();
308 (void) sprintf(line, "%s mask=%lu", fh, mask);
309 break;
310 case ACLPROC3_SETACL:
311 (void) sprintf(line, sum_nfsfh3());
312 break;
313 case ACLPROC3_GETXATTRDIR:
314 fh = sum_nfsfh3();
315 (void) sprintf(line, "%s create=%s", fh,
316 getxdr_bool() ? "true" : "false");
317 break;
318 default:
319 break;
320 }
321
322 check_retransmit(line, (ulong_t)xid);
323 } else {
324 (void) sprintf(line, "NFS_ACL R %s ",
325 procnames_short_v3[proc]);
326 line += strlen(line);
327 switch (proc) {
328 case ACLPROC3_GETACL:
329 (void) sum_nfsstat3(line);
330 break;
331 case ACLPROC3_SETACL:
332 (void) sum_nfsstat3(line);
333 break;
334 case ACLPROC3_GETXATTRDIR:
335 if (sum_nfsstat3(line) == 0) {
336 line += strlen(line);
337 (void) sprintf(line, sum_nfsfh3());
338 }
339 break;
340 default:
341 break;
342 }
343 }
344 }
345
346 if (flags & F_DTAIL) {
347 show_header("NFS_ACL: ", "Sun NFS_ACL", len);
348 show_space();
349 (void) sprintf(get_line(0, 0), "Proc = %d (%s)",
350 proc, procnames_long_v3[proc]);
351 if (type == CALL)
352 aclcall3(proc);
353 else
354 aclreply3(proc);
355 show_trailer();
356 }
357 }
358
359 static void
interpret_nfs_acl4(int flags,int type,int xid,int vers,int proc,char * data,int len)360 interpret_nfs_acl4(int flags, int type, int xid, int vers, int proc,
361 char *data, int len)
362 {
363 char *line;
364 char buff[2048];
365 int off, sz;
366 char *fh;
367 ulong_t mask;
368
369 if (proc < 0 || proc > MAXPROC_V4)
370 return;
371
372 if (flags & F_SUM) {
373 line = get_sum_line();
374
375 if (type == CALL) {
376 (void) sprintf(line, "NFS_ACL C %s",
377 procnames_short_v4[proc]);
378 line += strlen(line);
379 switch (proc) {
380 case ACLPROC4_GETACL:
381 fh = sum_nfsfh3();
382 mask = getxdr_u_long();
383 (void) sprintf(line, "%s mask=%lu", fh, mask);
384 break;
385 case ACLPROC4_SETACL:
386 (void) sprintf(line, sum_nfsfh3());
387 break;
388 default:
389 break;
390 }
391
392 check_retransmit(line, (ulong_t)xid);
393 } else {
394 (void) sprintf(line, "NFS_ACL R %s ",
395 procnames_short_v4[proc]);
396 line += strlen(line);
397 switch (proc) {
398 case ACLPROC4_GETACL:
399 (void) sum_nfsstat4(line);
400 break;
401 case ACLPROC4_SETACL:
402 (void) sum_nfsstat4(line);
403 break;
404 default:
405 break;
406 }
407 }
408 }
409
410 if (flags & F_DTAIL) {
411 show_header("NFS_ACL: ", "Sun NFS_ACL", len);
412 show_space();
413 (void) sprintf(get_line(0, 0), "Proc = %d (%s)",
414 proc, procnames_long_v4[proc]);
415 if (type == CALL)
416 aclcall4(proc);
417 else
418 aclreply4(proc);
419 show_trailer();
420 }
421 }
422
423 int
sum_nfsstat4(char * line)424 sum_nfsstat4(char *line)
425 {
426 ulong_t status;
427 char *p, *nfsstat4_to_name(int);
428
429 status = getxdr_long();
430 p = nfsstat4_to_name(status);
431 (void) strcpy(line, p);
432 return (status);
433 }
434
435 int
detail_nfsstat4()436 detail_nfsstat4()
437 {
438 ulong_t status;
439 char buff[64];
440 int pos;
441
442 pos = getxdr_pos();
443 status = sum_nfsstat4(buff);
444
445 (void) sprintf(get_line(pos, getxdr_pos()), "Status = %d (%s)",
446 status, buff);
447
448 return ((int)status);
449 }
450
451 /*
452 * Print out version 2 NFS_ACL call packets
453 */
454 static void
aclcall2(proc)455 aclcall2(proc)
456 int proc;
457 {
458
459 switch (proc) {
460 case ACLPROC2_GETACL:
461 detail_nfsfh();
462 detail_mask();
463 break;
464 case ACLPROC2_SETACL:
465 detail_nfsfh();
466 detail_secattr();
467 break;
468 case ACLPROC2_GETATTR:
469 detail_nfsfh();
470 break;
471 case ACLPROC2_ACCESS:
472 detail_nfsfh();
473 detail_access2();
474 break;
475 default:
476 break;
477 }
478 }
479
480 /*
481 * Print out version 2 NFS_ACL reply packets
482 */
483 static void
aclreply2(proc)484 aclreply2(proc)
485 int proc;
486 {
487
488 switch (proc) {
489 case ACLPROC2_GETACL:
490 if (detail_nfsstat() == 0) {
491 detail_fattr();
492 detail_secattr();
493 }
494 break;
495 case ACLPROC2_SETACL:
496 if (detail_nfsstat() == 0)
497 detail_fattr();
498 break;
499 case ACLPROC2_GETATTR:
500 if (detail_nfsstat() == 0)
501 detail_fattr();
502 break;
503 case ACLPROC2_ACCESS:
504 if (detail_nfsstat() == 0) {
505 detail_fattr();
506 detail_access2();
507 }
508 break;
509 default:
510 break;
511 }
512 }
513
514 /*
515 * Print out version 3 NFS_ACL call packets
516 */
517 static void
aclcall3(proc)518 aclcall3(proc)
519 int proc;
520 {
521
522 switch (proc) {
523 case ACLPROC3_GETACL:
524 detail_nfsfh3();
525 detail_mask();
526 break;
527 case ACLPROC3_SETACL:
528 detail_nfsfh3();
529 detail_secattr();
530 break;
531 default:
532 break;
533 }
534 }
535
536 /*
537 * Print out version 3 NFS_ACL reply packets
538 */
539 static void
aclreply3(proc)540 aclreply3(proc)
541 int proc;
542 {
543
544 switch (proc) {
545 case ACLPROC3_GETACL:
546 if (detail_nfsstat3() == 0) {
547 detail_post_op_attr("");
548 detail_secattr();
549 }
550 break;
551 case ACLPROC3_SETACL:
552 if (detail_nfsstat3() == 0)
553 detail_post_op_attr("");
554 break;
555 default:
556 break;
557 }
558 }
559
560 /*
561 * Print out version 4 NFS_ACL call packets
562 */
563 static void
aclcall4(proc)564 aclcall4(proc)
565 int proc;
566 {
567
568 switch (proc) {
569 case ACLPROC4_GETACL:
570 detail_nfsfh3();
571 detail_mask();
572 break;
573 case ACLPROC4_SETACL:
574 detail_nfsfh3();
575 detail_secattr();
576 break;
577 default:
578 break;
579 }
580 }
581
582 /*
583 * Print out version 4 NFS_ACL reply packets
584 */
585 static void
aclreply4(proc)586 aclreply4(proc)
587 int proc;
588 {
589
590 switch (proc) {
591 case ACLPROC4_GETACL:
592 if (detail_nfsstat4() == 0) {
593 detail_post_op_attr("");
594 detail_secattr();
595 }
596 break;
597 case ACLPROC4_SETACL:
598 if (detail_nfsstat4() == 0)
599 detail_post_op_attr("");
600 break;
601 default:
602 break;
603 }
604 }
605
606 static void
detail_access2()607 detail_access2()
608 {
609 uint_t bits;
610
611 bits = showxdr_u_long("Access bits = 0x%08x");
612 (void) sprintf(get_line(0, 0), " %s",
613 getflag(bits, ACCESS2_READ, "Read", "(no read)"));
614 (void) sprintf(get_line(0, 0), " %s",
615 getflag(bits, ACCESS2_LOOKUP, "Lookup", "(no lookup)"));
616 (void) sprintf(get_line(0, 0), " %s",
617 getflag(bits, ACCESS2_MODIFY, "Modify", "(no modify)"));
618 (void) sprintf(get_line(0, 0), " %s",
619 getflag(bits, ACCESS2_EXTEND, "Extend", "(no extend)"));
620 (void) sprintf(get_line(0, 0), " %s",
621 getflag(bits, ACCESS2_DELETE, "Delete", "(no delete)"));
622 (void) sprintf(get_line(0, 0), " %s",
623 getflag(bits, ACCESS2_EXECUTE, "Execute", "(no execute)"));
624 }
625
626 static char *
sum_access2()627 sum_access2()
628 {
629 int bits;
630 static char buff[22];
631
632 bits = getxdr_u_long();
633 buff[0] = '\0';
634
635 if (bits & ACCESS2_READ)
636 (void) strcat(buff, "read,");
637 if (bits & ACCESS2_LOOKUP)
638 (void) strcat(buff, "lookup,");
639 if (bits & ACCESS2_MODIFY)
640 (void) strcat(buff, "modify,");
641 if (bits & ACCESS2_EXTEND)
642 (void) strcat(buff, "extend,");
643 if (bits & ACCESS2_DELETE)
644 (void) strcat(buff, "delete,");
645 if (bits & ACCESS2_EXECUTE)
646 (void) strcat(buff, "execute,");
647 if (buff[0] != '\0')
648 buff[strlen(buff) - 1] = '\0';
649
650 return (buff);
651 }
652
653 static void
detail_mask()654 detail_mask()
655 {
656 ulong_t mask;
657
658 mask = showxdr_u_long("Mask = 0x%lx");
659 (void) sprintf(get_line(0, 0), " %s",
660 getflag(mask, NA_ACL, "aclent", "(no aclent)"));
661 (void) sprintf(get_line(0, 0), " %s",
662 getflag(mask, NA_ACLCNT, "aclcnt", "(no aclcnt)"));
663 (void) sprintf(get_line(0, 0), " %s",
664 getflag(mask, NA_DFACL, "dfaclent", "(no dfaclent)"));
665 (void) sprintf(get_line(0, 0), " %s",
666 getflag(mask, NA_DFACLCNT, "dfaclcnt", "(no dfaclcnt)"));
667 }
668
669 static void
detail_secattr()670 detail_secattr()
671 {
672
673 detail_mask();
674 showxdr_long("Aclcnt = %d");
675 detail_aclent();
676 showxdr_long("Dfaclcnt = %d");
677 detail_aclent();
678 }
679
680 static void
detail_aclent()681 detail_aclent()
682 {
683 int count;
684 int type;
685 int id;
686 ushort_t perm;
687
688 count = getxdr_long();
689 while (count-- > 0) {
690 type = getxdr_long();
691 id = getxdr_long();
692 perm = getxdr_u_short();
693 switch (type) {
694 case NA_USER:
695 (void) sprintf(get_line(0, 0), "\tuser:%s:%s",
696 detail_uname(id), detail_perm(perm));
697 break;
698 case NA_USER_OBJ:
699 (void) sprintf(get_line(0, 0), "\tuser::%s",
700 detail_perm(perm));
701 break;
702 case NA_GROUP:
703 (void) sprintf(get_line(0, 0), "\tgroup:%s:%s",
704 detail_gname(id), detail_perm(perm));
705 break;
706 case NA_GROUP_OBJ:
707 (void) sprintf(get_line(0, 0), "\tgroup::%s",
708 detail_perm(perm));
709 break;
710 case NA_CLASS_OBJ:
711 (void) sprintf(get_line(0, 0), "\tmask:%s",
712 detail_perm(perm));
713 break;
714 case NA_OTHER_OBJ:
715 (void) sprintf(get_line(0, 0), "\tother:%s",
716 detail_perm(perm));
717 break;
718 case NA_DEF_USER:
719 (void) sprintf(get_line(0, 0), "\tdefault:user:%s:%s",
720 detail_uname(id), detail_perm(perm));
721 break;
722 case NA_DEF_USER_OBJ:
723 (void) sprintf(get_line(0, 0), "\tdefault:user::%s",
724 detail_perm(perm));
725 break;
726 case NA_DEF_GROUP:
727 (void) sprintf(get_line(0, 0), "\tdefault:group:%s:%s",
728 detail_gname(id), detail_perm(perm));
729 break;
730 case NA_DEF_GROUP_OBJ:
731 (void) sprintf(get_line(0, 0), "\tdefault:group::%s",
732 detail_perm(perm));
733 break;
734 case NA_DEF_CLASS_OBJ:
735 (void) sprintf(get_line(0, 0), "\tdefault:mask:%s",
736 detail_perm(perm));
737 break;
738 case NA_DEF_OTHER_OBJ:
739 (void) sprintf(get_line(0, 0), "\tdefault:other:%s",
740 detail_perm(perm));
741 break;
742 default:
743 (void) sprintf(get_line(0, 0), "\tunrecognized entry");
744 break;
745 }
746 }
747 }
748
749 static char *
detail_uname(uid_t uid)750 detail_uname(uid_t uid)
751 {
752 struct passwd *pwd;
753 static char uidp[10];
754
755 pwd = getpwuid(uid);
756 if (pwd == NULL) {
757 sprintf(uidp, "%d", uid);
758 return (uidp);
759 }
760 return (pwd->pw_name);
761 }
762
763 static char *
detail_gname(gid_t gid)764 detail_gname(gid_t gid)
765 {
766 struct group *grp;
767 static char gidp[10];
768
769 grp = getgrgid(gid);
770 if (grp == NULL) {
771 sprintf(gidp, "%d", gid);
772 return (gidp);
773 }
774 return (grp->gr_name);
775 }
776
777 static char *perms[] = {
778 "---",
779 "--x",
780 "-w-",
781 "-wx",
782 "r--",
783 "r-x",
784 "rw-",
785 "rwx"
786 };
787 static char *
detail_perm(ushort_t perm)788 detail_perm(ushort_t perm)
789 {
790
791 if (perm >= sizeof (perms) / sizeof (perms[0]))
792 return ("?");
793 return (perms[perm]);
794 }
795