- update my name. No obj-code changes ;)
[platform/upstream/busybox.git] / coreutils / test.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * test implementation for busybox
4  *
5  * Copyright (c) by a whole pile of folks:
6  *
7  *     test(1); version 7-like  --  author Erik Baalbergen
8  *     modified by Eric Gisin to be used as built-in.
9  *     modified by Arnold Robbins to add SVR3 compatibility
10  *     (-x -c -b -p -u -g -k) plus Korn's -L -nt -ot -ef and new -S (socket).
11  *     modified by J.T. Conklin for NetBSD.
12  *     modified by Herbert Xu to be used as built-in in ash.
13  *     modified by Erik Andersen <andersen@codepoet.org> to be used
14  *     in busybox.
15  *     modified by Bernhard Reutner-Fischer to be useable (i.e. a bit less bloaty).
16  *
17  * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
18  *
19  * Original copyright notice states:
20  *     "This program is in the Public Domain."
21  */
22
23 #include "libbb.h"
24 #include <setjmp.h>
25
26 /* This is a NOFORK applet. Be very careful! */
27
28 /* test_main() is called from shells, and we need to be extra careful here.
29  * This is true regardless of PREFER_APPLETS and STANDALONE_SHELL
30  * state. */
31
32
33 /* test(1) accepts the following grammar:
34         oexpr   ::= aexpr | aexpr "-o" oexpr ;
35         aexpr   ::= nexpr | nexpr "-a" aexpr ;
36         nexpr   ::= primary | "!" primary
37         primary ::= unary-operator operand
38                 | operand binary-operator operand
39                 | operand
40                 | "(" oexpr ")"
41                 ;
42         unary-operator ::= "-r"|"-w"|"-x"|"-f"|"-d"|"-c"|"-b"|"-p"|
43                 "-u"|"-g"|"-k"|"-s"|"-t"|"-z"|"-n"|"-o"|"-O"|"-G"|"-L"|"-S";
44
45         binary-operator ::= "="|"=="|"!="|"-eq"|"-ne"|"-ge"|"-gt"|"-le"|"-lt"|
46                         "-nt"|"-ot"|"-ef";
47         operand ::= <any legal UNIX file name>
48 */
49
50 #define TEST_DEBUG 0
51
52 enum token {
53         EOI,
54         FILRD,
55         FILWR,
56         FILEX,
57         FILEXIST,
58         FILREG,
59         FILDIR,
60         FILCDEV,
61         FILBDEV,
62         FILFIFO,
63         FILSOCK,
64         FILSYM,
65         FILGZ,
66         FILTT,
67         FILSUID,
68         FILSGID,
69         FILSTCK,
70         FILNT,
71         FILOT,
72         FILEQ,
73         FILUID,
74         FILGID,
75         STREZ,
76         STRNZ,
77         STREQ,
78         STRNE,
79         STRLT,
80         STRGT,
81         INTEQ,
82         INTNE,
83         INTGE,
84         INTGT,
85         INTLE,
86         INTLT,
87         UNOT,
88         BAND,
89         BOR,
90         LPAREN,
91         RPAREN,
92         OPERAND
93 };
94 #define is_int_op(a)      (((unsigned char)((a) - INTEQ)) <= 5)
95 #define is_str_op(a)      (((unsigned char)((a) - STREZ)) <= 5)
96 #define is_file_op(a)     (((unsigned char)((a) - FILNT)) <= 2)
97 #define is_file_access(a) (((unsigned char)((a) - FILRD)) <= 2)
98 #define is_file_type(a)   (((unsigned char)((a) - FILREG)) <= 5)
99 #define is_file_bit(a)    (((unsigned char)((a) - FILSUID)) <= 2)
100
101 #if TEST_DEBUG
102 int depth;
103 #define nest_msg(...) do { \
104         depth++; \
105         fprintf(stderr, "%*s", depth*2, ""); \
106         fprintf(stderr, __VA_ARGS__); \
107 } while (0)
108 #define unnest_msg(...) do { \
109         fprintf(stderr, "%*s", depth*2, ""); \
110         fprintf(stderr, __VA_ARGS__); \
111         depth--; \
112 } while (0)
113 #define dbg_msg(...) do { \
114         fprintf(stderr, "%*s", depth*2, ""); \
115         fprintf(stderr, __VA_ARGS__); \
116 } while (0)
117 #define unnest_msg_and_return(expr, ...) do { \
118         number_t __res = (expr); \
119         fprintf(stderr, "%*s", depth*2, ""); \
120         fprintf(stderr, __VA_ARGS__, res); \
121         depth--; \
122         return __res; \
123 } while (0)
124 static const char *const TOKSTR[] = {
125         "EOI",
126         "FILRD",
127         "FILWR",
128         "FILEX",
129         "FILEXIST",
130         "FILREG",
131         "FILDIR",
132         "FILCDEV",
133         "FILBDEV",
134         "FILFIFO",
135         "FILSOCK",
136         "FILSYM",
137         "FILGZ",
138         "FILTT",
139         "FILSUID",
140         "FILSGID",
141         "FILSTCK",
142         "FILNT",
143         "FILOT",
144         "FILEQ",
145         "FILUID",
146         "FILGID",
147         "STREZ",
148         "STRNZ",
149         "STREQ",
150         "STRNE",
151         "STRLT",
152         "STRGT",
153         "INTEQ",
154         "INTNE",
155         "INTGE",
156         "INTGT",
157         "INTLE",
158         "INTLT",
159         "UNOT",
160         "BAND",
161         "BOR",
162         "LPAREN",
163         "RPAREN",
164         "OPERAND"
165 };
166 #else
167 #define nest_msg(...)   ((void)0)
168 #define unnest_msg(...) ((void)0)
169 #define dbg_msg(...)    ((void)0)
170 #define unnest_msg_and_return(expr, ...) return expr
171 #endif
172
173 enum token_types {
174         UNOP,
175         BINOP,
176         BUNOP,
177         BBINOP,
178         PAREN
179 };
180
181 struct operator_t {
182         char op_text[4];
183         unsigned char op_num, op_type;
184 };
185
186 static const struct operator_t ops[] = {
187         { "-r", FILRD   , UNOP   },
188         { "-w", FILWR   , UNOP   },
189         { "-x", FILEX   , UNOP   },
190         { "-e", FILEXIST, UNOP   },
191         { "-f", FILREG  , UNOP   },
192         { "-d", FILDIR  , UNOP   },
193         { "-c", FILCDEV , UNOP   },
194         { "-b", FILBDEV , UNOP   },
195         { "-p", FILFIFO , UNOP   },
196         { "-u", FILSUID , UNOP   },
197         { "-g", FILSGID , UNOP   },
198         { "-k", FILSTCK , UNOP   },
199         { "-s", FILGZ   , UNOP   },
200         { "-t", FILTT   , UNOP   },
201         { "-z", STREZ   , UNOP   },
202         { "-n", STRNZ   , UNOP   },
203         { "-h", FILSYM  , UNOP   },    /* for backwards compat */
204
205         { "-O" , FILUID , UNOP   },
206         { "-G" , FILGID , UNOP   },
207         { "-L" , FILSYM , UNOP   },
208         { "-S" , FILSOCK, UNOP   },
209         { "="  , STREQ  , BINOP  },
210         { "==" , STREQ  , BINOP  },
211         { "!=" , STRNE  , BINOP  },
212         { "<"  , STRLT  , BINOP  },
213         { ">"  , STRGT  , BINOP  },
214         { "-eq", INTEQ  , BINOP  },
215         { "-ne", INTNE  , BINOP  },
216         { "-ge", INTGE  , BINOP  },
217         { "-gt", INTGT  , BINOP  },
218         { "-le", INTLE  , BINOP  },
219         { "-lt", INTLT  , BINOP  },
220         { "-nt", FILNT  , BINOP  },
221         { "-ot", FILOT  , BINOP  },
222         { "-ef", FILEQ  , BINOP  },
223         { "!"  , UNOT   , BUNOP  },
224         { "-a" , BAND   , BBINOP },
225         { "-o" , BOR    , BBINOP },
226         { "("  , LPAREN , PAREN  },
227         { ")"  , RPAREN , PAREN  },
228 };
229
230
231 #if ENABLE_FEATURE_TEST_64
232 typedef int64_t number_t;
233 #else
234 typedef int number_t;
235 #endif
236
237
238 /* We try to minimize both static and stack usage. */
239 struct test_statics {
240         char **args;
241         /* set only by check_operator(), either to bogus struct
242          * or points to matching operator_t struct. Never NULL. */
243         const struct operator_t *last_operator;
244         gid_t *group_array;
245         int ngroups;
246         jmp_buf leaving;
247 };
248
249 /* See test_ptr_hack.c */
250 extern struct test_statics *const test_ptr_to_statics;
251
252 #define S (*test_ptr_to_statics)
253 #define args            (S.args         )
254 #define last_operator   (S.last_operator)
255 #define group_array     (S.group_array  )
256 #define ngroups         (S.ngroups      )
257 #define leaving         (S.leaving      )
258
259 #define INIT_S() do { \
260         (*(struct test_statics**)&test_ptr_to_statics) = xzalloc(sizeof(S)); \
261         barrier(); \
262 } while (0)
263 #define DEINIT_S() do { \
264         free(test_ptr_to_statics); \
265 } while (0)
266
267 static number_t primary(enum token n);
268
269 static void syntax(const char *op, const char *msg) NORETURN;
270 static void syntax(const char *op, const char *msg)
271 {
272         if (op && *op) {
273                 bb_error_msg("%s: %s", op, msg);
274         } else {
275                 bb_error_msg("%s: %s"+4, msg);
276         }
277         longjmp(leaving, 2);
278 }
279
280 /* atoi with error detection */
281 //XXX: FIXME: duplicate of existing libbb function?
282 static number_t getn(const char *s)
283 {
284         char *p;
285 #if ENABLE_FEATURE_TEST_64
286         long long r;
287 #else
288         long r;
289 #endif
290
291         errno = 0;
292 #if ENABLE_FEATURE_TEST_64
293         r = strtoll(s, &p, 10);
294 #else
295         r = strtol(s, &p, 10);
296 #endif
297
298         if (errno != 0)
299                 syntax(s, "out of range");
300
301         if (*(skip_whitespace(p)))
302                 syntax(s, "bad number");
303
304         return r;
305 }
306
307 /* UNUSED
308 static int newerf(const char *f1, const char *f2)
309 {
310         struct stat b1, b2;
311
312         return (stat(f1, &b1) == 0 &&
313                         stat(f2, &b2) == 0 && b1.st_mtime > b2.st_mtime);
314 }
315
316 static int olderf(const char *f1, const char *f2)
317 {
318         struct stat b1, b2;
319
320         return (stat(f1, &b1) == 0 &&
321                         stat(f2, &b2) == 0 && b1.st_mtime < b2.st_mtime);
322 }
323
324 static int equalf(const char *f1, const char *f2)
325 {
326         struct stat b1, b2;
327
328         return (stat(f1, &b1) == 0 &&
329                         stat(f2, &b2) == 0 &&
330                         b1.st_dev == b2.st_dev && b1.st_ino == b2.st_ino);
331 }
332 */
333
334
335 static enum token check_operator(char *s)
336 {
337         static const struct operator_t no_op = {
338                 .op_num = -1,
339                 .op_type = -1
340         };
341         const struct operator_t *op;
342
343         last_operator = &no_op;
344         if (s == NULL) {
345                 return EOI;
346         }
347
348         op = ops;
349         do {
350                 if (strcmp(s, op->op_text) == 0) {
351                         last_operator = op;
352                         return op->op_num;
353                 }
354                 op++;
355         } while (op < ops + ARRAY_SIZE(ops));
356
357         return OPERAND;
358 }
359
360
361 static int binop(void)
362 {
363         const char *opnd1, *opnd2;
364         const struct operator_t *op;
365         number_t val1, val2;
366
367         opnd1 = *args;
368         check_operator(*++args);
369         op = last_operator;
370
371         opnd2 = *++args;
372         if (opnd2 == NULL)
373                 syntax(op->op_text, "argument expected");
374
375         if (is_int_op(op->op_num)) {
376                 val1 = getn(opnd1);
377                 val2 = getn(opnd2);
378                 if (op->op_num == INTEQ)
379                         return val1 == val2;
380                 if (op->op_num == INTNE)
381                         return val1 != val2;
382                 if (op->op_num == INTGE)
383                         return val1 >= val2;
384                 if (op->op_num == INTGT)
385                         return val1 >  val2;
386                 if (op->op_num == INTLE)
387                         return val1 <= val2;
388                 if (op->op_num == INTLT)
389                         return val1 <  val2;
390         }
391         if (is_str_op(op->op_num)) {
392                 val1 = strcmp(opnd1, opnd2);
393                 if (op->op_num == STREQ)
394                         return val1 == 0;
395                 if (op->op_num == STRNE)
396                         return val1 != 0;
397                 if (op->op_num == STRLT)
398                         return val1 < 0;
399                 if (op->op_num == STRGT)
400                         return val1 > 0;
401         }
402         /* We are sure that these three are by now the only binops we didn't check
403          * yet, so we do not check if the class is correct:
404          */
405 /*      if (is_file_op(op->op_num)) */
406         {
407                 struct stat b1, b2;
408
409                 if (stat(opnd1, &b1) || stat(opnd2, &b2))
410                         return 0; /* false, since at least one stat failed */
411                 if (op->op_num == FILNT)
412                         return b1.st_mtime > b2.st_mtime;
413                 if (op->op_num == FILOT)
414                         return b1.st_mtime < b2.st_mtime;
415                 if (op->op_num == FILEQ)
416                         return b1.st_dev == b2.st_dev && b1.st_ino == b2.st_ino;
417         }
418         return 1; /* NOTREACHED */
419 }
420
421
422 static void initialize_group_array(void)
423 {
424         ngroups = getgroups(0, NULL);
425         if (ngroups > 0) {
426                 /* FIXME: ash tries so hard to not die on OOM,
427                  * and we spoil it with just one xrealloc here */
428                 /* We realloc, because test_main can be entered repeatedly by shell.
429                  * Testcase (ash): 'while true; do test -x some_file; done'
430                  * and watch top. (some_file must have owner != you) */
431                 group_array = xrealloc(group_array, ngroups * sizeof(gid_t));
432                 getgroups(ngroups, group_array);
433         }
434 }
435
436
437 /* Return non-zero if GID is one that we have in our groups list. */
438 //XXX: FIXME: duplicate of existing libbb function?
439 // see toplevel TODO file:
440 // possible code duplication ingroup() and is_a_group_member()
441 static int is_a_group_member(gid_t gid)
442 {
443         int i;
444
445         /* Short-circuit if possible, maybe saving a call to getgroups(). */
446         if (gid == getgid() || gid == getegid())
447                 return 1;
448
449         if (ngroups == 0)
450                 initialize_group_array();
451
452         /* Search through the list looking for GID. */
453         for (i = 0; i < ngroups; i++)
454                 if (gid == group_array[i])
455                         return 1;
456
457         return 0;
458 }
459
460
461 /* Do the same thing access(2) does, but use the effective uid and gid,
462    and don't make the mistake of telling root that any file is
463    executable. */
464 static int test_eaccess(char *path, int mode)
465 {
466         struct stat st;
467         unsigned int euid = geteuid();
468
469         if (stat(path, &st) < 0)
470                 return -1;
471
472         if (euid == 0) {
473                 /* Root can read or write any file. */
474                 if (mode != X_OK)
475                         return 0;
476
477                 /* Root can execute any file that has any one of the execute
478                    bits set. */
479                 if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
480                         return 0;
481         }
482
483         if (st.st_uid == euid)  /* owner */
484                 mode <<= 6;
485         else if (is_a_group_member(st.st_gid))
486                 mode <<= 3;
487
488         if (st.st_mode & mode)
489                 return 0;
490
491         return -1;
492 }
493
494
495 static int filstat(char *nm, enum token mode)
496 {
497         struct stat s;
498         unsigned i = i; /* gcc 3.x thinks it can be used uninitialized */
499
500         if (mode == FILSYM) {
501 #ifdef S_IFLNK
502                 if (lstat(nm, &s) == 0) {
503                         i = S_IFLNK;
504                         goto filetype;
505                 }
506 #endif
507                 return 0;
508         }
509
510         if (stat(nm, &s) != 0)
511                 return 0;
512         if (mode == FILEXIST)
513                 return 1;
514         if (is_file_access(mode)) {
515                 if (mode == FILRD)
516                         i = R_OK;
517                 if (mode == FILWR)
518                         i = W_OK;
519                 if (mode == FILEX)
520                         i = X_OK;
521                 return test_eaccess(nm, i) == 0;
522         }
523         if (is_file_type(mode)) {
524                 if (mode == FILREG)
525                         i = S_IFREG;
526                 if (mode == FILDIR)
527                         i = S_IFDIR;
528                 if (mode == FILCDEV)
529                         i = S_IFCHR;
530                 if (mode == FILBDEV)
531                         i = S_IFBLK;
532                 if (mode == FILFIFO) {
533 #ifdef S_IFIFO
534                         i = S_IFIFO;
535 #else
536                         return 0;
537 #endif
538                 }
539                 if (mode == FILSOCK) {
540 #ifdef S_IFSOCK
541                         i = S_IFSOCK;
542 #else
543                         return 0;
544 #endif
545                 }
546  filetype:
547                 return ((s.st_mode & S_IFMT) == i);
548         }
549         if (is_file_bit(mode)) {
550                 if (mode == FILSUID)
551                         i = S_ISUID;
552                 if (mode == FILSGID)
553                         i = S_ISGID;
554                 if (mode == FILSTCK)
555                         i = S_ISVTX;
556                 return ((s.st_mode & i) != 0);
557         }
558         if (mode == FILGZ)
559                 return s.st_size > 0L;
560         if (mode == FILUID)
561                 return s.st_uid == geteuid();
562         if (mode == FILGID)
563                 return s.st_gid == getegid();
564         return 1; /* NOTREACHED */
565 }
566
567
568 static number_t nexpr(enum token n)
569 {
570         number_t res;
571
572         nest_msg(">nexpr(%s)\n", TOKSTR[n]);
573         if (n == UNOT) {
574                 res = !nexpr(check_operator(*++args));
575                 unnest_msg("<nexpr:%lld\n", res);
576                 return res;
577         }
578         res = primary(n);
579         unnest_msg("<nexpr:%lld\n", res);
580         return res;
581 }
582
583
584 static number_t aexpr(enum token n)
585 {
586         number_t res;
587
588         nest_msg(">aexpr(%s)\n", TOKSTR[n]);
589         res = nexpr(n);
590         dbg_msg("aexpr: nexpr:%lld, next args:%s\n", res, args[1]);
591         if (check_operator(*++args) == BAND) {
592                 dbg_msg("aexpr: arg is AND, next args:%s\n", args[1]);
593                 res = aexpr(check_operator(*++args)) && res;
594                 unnest_msg("<aexpr:%lld\n", res);
595                 return res;
596         }
597         args--;
598         unnest_msg("<aexpr:%lld, args:%s\n", res, args[0]);
599         return res;
600 }
601
602
603 static number_t oexpr(enum token n)
604 {
605         number_t res;
606
607         nest_msg(">oexpr(%s)\n", TOKSTR[n]);
608         res = aexpr(n);
609         dbg_msg("oexpr: aexpr:%lld, next args:%s\n", res, args[1]);
610         if (check_operator(*++args) == BOR) {
611                 dbg_msg("oexpr: next arg is OR, next args:%s\n", args[1]);
612                 res = oexpr(check_operator(*++args)) || res;
613                 unnest_msg("<oexpr:%lld\n", res);
614                 return res;
615         }
616         args--;
617         unnest_msg("<oexpr:%lld, args:%s\n", res, args[0]);
618         return res;
619 }
620
621
622 static number_t primary(enum token n)
623 {
624 #if TEST_DEBUG
625         number_t res = res; /* for compiler */
626 #else
627         number_t res;
628 #endif
629         const struct operator_t *args0_op;
630
631         nest_msg(">primary(%s)\n", TOKSTR[n]);
632         if (n == EOI) {
633                 syntax(NULL, "argument expected");
634         }
635         if (n == LPAREN) {
636                 res = oexpr(check_operator(*++args));
637                 if (check_operator(*++args) != RPAREN)
638                         syntax(NULL, "closing paren expected");
639                 unnest_msg("<primary:%lld\n", res);
640                 return res;
641         }
642
643         /* coreutils 6.9 checks "is args[1] binop and args[2] exist?" first,
644          * do the same */
645         args0_op = last_operator;
646         /* last_operator = operator at args[1] */
647         if (check_operator(args[1]) != EOI) { /* if args[1] != NULL */
648                 if (args[2]) {
649                         // coreutils also does this:
650                         // if (args[3] && args[0]="-l" && args[2] is BINOP)
651                         //      return binop(1 /* prepended by -l */);
652                         if (last_operator->op_type == BINOP)
653                                 unnest_msg_and_return(binop(), "<primary: binop:%lld\n");
654                 }
655         }
656         /* check "is args[0] unop?" second */
657         if (args0_op->op_type == UNOP) {
658                 /* unary expression */
659                 if (args[1] == NULL)
660 //                      syntax(args0_op->op_text, "argument expected");
661                         goto check_emptiness;
662                 args++;
663                 if (n == STREZ)
664                         unnest_msg_and_return(args[0][0] == '\0', "<primary:%lld\n");
665                 if (n == STRNZ)
666                         unnest_msg_and_return(args[0][0] != '\0', "<primary:%lld\n");
667                 if (n == FILTT)
668                         unnest_msg_and_return(isatty(getn(*args)), "<primary: isatty(%s)%lld\n", *args);
669                 unnest_msg_and_return(filstat(*args, n), "<primary: filstat(%s):%lld\n", *args);
670         }
671
672         /*check_operator(args[1]); - already done */
673         if (last_operator->op_type == BINOP) {
674                 /* args[2] is known to be NULL, isn't it bound to fail? */
675                 unnest_msg_and_return(binop(), "<primary:%lld\n");
676         }
677  check_emptiness:
678         unnest_msg_and_return(args[0][0] != '\0', "<primary:%lld\n");
679 }
680
681
682 int test_main(int argc, char **argv)
683 {
684         int res;
685         const char *arg0;
686 //      bool negate = 0;
687
688         arg0 = bb_basename(argv[0]);
689         if (arg0[0] == '[') {
690                 --argc;
691                 if (!arg0[1]) { /* "[" ? */
692                         if (NOT_LONE_CHAR(argv[argc], ']')) {
693                                 bb_error_msg("missing ]");
694                                 return 2;
695                         }
696                 } else { /* assuming "[[" */
697                         if (strcmp(argv[argc], "]]") != 0) {
698                                 bb_error_msg("missing ]]");
699                                 return 2;
700                         }
701                 }
702                 argv[argc] = NULL;
703         }
704
705         /* We must do DEINIT_S() prior to returning */
706         INIT_S();
707
708         res = setjmp(leaving);
709         if (res)
710                 goto ret;
711
712         /* resetting ngroups is probably unnecessary.  it will
713          * force a new call to getgroups(), which prevents using
714          * group data fetched during a previous call.  but the
715          * only way the group data could be stale is if there's
716          * been an intervening call to setgroups(), and this
717          * isn't likely in the case of a shell.  paranoia
718          * prevails...
719          */
720         ngroups = 0;
721
722         //argc--;
723         argv++;
724
725         /* Implement special cases from POSIX.2, section 4.62.4 */
726         if (!argv[0]) { /* "test" */
727                 res = 1;
728                 goto ret;
729         }
730 #if 0
731 // Now it's fixed in the parser and should not be needed
732         if (LONE_CHAR(argv[0], '!') && argv[1]) {
733                 negate = 1;
734                 //argc--;
735                 argv++;
736         }
737         if (!argv[1]) { /* "test [!] arg" */
738                 res = (*argv[0] == '\0');
739                 goto ret;
740         }
741         if (argv[2] && !argv[3]) {
742                 check_operator(argv[1]);
743                 if (last_operator->op_type == BINOP) {
744                         /* "test [!] arg1 <binary_op> arg2" */
745                         args = &argv[0];
746                         res = (binop() == 0);
747                         goto ret;
748                 }
749         }
750
751         /* Some complex expression. Undo '!' removal */
752         if (negate) {
753                 negate = 0;
754                 //argc++;
755                 argv--;
756         }
757 #endif
758         args = &argv[0];
759         res = !oexpr(check_operator(*args));
760
761         if (*args != NULL && *++args != NULL) {
762                 /* TODO: example when this happens? */
763                 bb_error_msg("%s: unknown operand", *args);
764                 res = 2;
765         }
766  ret:
767         DEINIT_S();
768 //      return negate ? !res : res;
769         return res;
770 }