1 /*
2 * list.c
3 *
4 * (C)1998-2011 by Marc Huber <Marc.Huber@web.de>
5 * All rights reserved.
6 *
7 * $Id: list.c,v 1.22 2015/03/14 06:11:26 marc Exp marc $
8 *
9 */
10
11 #include "headers.h"
12 #include "glob.h"
13 #include "misc/rb.h"
14 #include "misc/tokenize.h"
15 #include <pwd.h>
16 #include <grp.h>
17
18 static const char rcsid[] __attribute__ ((used)) = "$Id: list.c,v 1.22 2015/03/14 06:11:26 marc Exp marc $";
19
20 static int list_dir(struct context *, char *, char *);
21 static void list_dir_details(struct context *, int);
22
23 #define list_basename(A) ((strlen (A) <= ctx->rootlen) ? "/" : basename (A))
24
25 rb_tree_t *mimetypes = NULL;
26
27 struct mime_type {
28 char *extension;
29 char mimetype[1];
30 };
31
compare_extension(const void * a,const void * b)32 static int compare_extension(const void *a, const void *b)
33 {
34 return strcasecmp(((struct mime_type *) a)->extension, ((struct mime_type *) b)->extension);
35 }
36
free_payload(void * payload)37 static void free_payload(void *payload)
38 {
39 free(payload);
40 }
41
lookup_mimetype(char * name)42 static char *lookup_mimetype(char *name)
43 {
44 char *e = strrchr(name, '.');
45 if (e) {
46 rb_node_t *rb;
47 static struct mime_type *mt_last = NULL;
48 struct mime_type mt;
49
50 if (mt_last && !strcasecmp(e, mt_last->extension))
51 return mt_last->mimetype;
52
53 mt.extension = e + 1;
54 if ((rb = RB_search(mimetypes, &mt))) {
55 mt_last = RB_payload(rb, struct mime_type *);
56 return mt_last->mimetype;
57 }
58 }
59 return NULL;
60 }
61
read_mimetypes(char * file)62 void read_mimetypes(char *file)
63 {
64 char inbuf[8000];
65 size_t offset = 0;
66 ssize_t inlength;
67 char *linestart = inbuf;
68 char *lineend;
69
70 int fn = open(file, O_RDONLY);
71 if (fn < 0) {
72 logerr("open (%s)", file);
73 return;
74 }
75
76 while ((inlength = read(fn, inbuf + offset, sizeof(inbuf) - 1 - offset)) > 0) {
77 inlength += offset;
78 inbuf[inlength] = 0;
79 linestart = inbuf;
80
81 while ((lineend = strchr(linestart, '\n'))) {
82 #define VECTOR_SIZE 99
83 char *vector[VECTOR_SIZE];
84 char **a = vector;
85 *lineend = 0;
86
87 tokenize(linestart, vector, VECTOR_SIZE);
88
89 if (*a && **a != '#' && strchr(*a, '/')) {
90 char *mt = *a++;
91 ssize_t mtlen = strlen(mt);
92 for (; *a; a++) {
93 struct mime_type *keyval;
94 if (!mimetypes)
95 mimetypes = RB_tree_new(compare_extension, free_payload);
96 keyval = Xcalloc(1, sizeof(struct mime_type) + 1 + strlen(*a) + mtlen);
97 strcpy(keyval->mimetype, mt);
98 keyval->extension = keyval->mimetype + mtlen + 1;
99 strcpy(keyval->extension, *a);
100 RB_search_and_delete(mimetypes, keyval);
101 RB_insert(mimetypes, keyval);
102 }
103 }
104 linestart = lineend + 1;
105 }
106
107 offset = inbuf + inlength - linestart;
108 if (offset)
109 memmove(inbuf, linestart, offset);
110 }
111 close(fn);
112 }
113
check_gids(struct context * ctx,gid_t gid)114 int check_gids(struct context *ctx, gid_t gid)
115 {
116 int i;
117 for (i = 0; i < ctx->gids_size; i++)
118 if (gid == ctx->gids[i])
119 return -1;
120 return 0;
121 }
122
123 struct id_item {
124 int id;
125 char name[1];
126 };
127
compare_id(const void * a,const void * b)128 static int compare_id(const void *a, const void *b)
129 {
130 return (((struct id_item *) a)->id - ((struct id_item *) b)->id);
131 }
132
lookup_uid(struct context * ctx,uid_t uid)133 char *lookup_uid(struct context *ctx, uid_t uid)
134 {
135 struct passwd *pw;
136 char *u = NULL;
137 static rb_tree_t *cache = NULL;
138 static uid_t last_uid;
139 static char *last_user = NULL;
140 rb_node_t *t;
141 struct id_item idi, *i;
142
143 if (!ctx->resolve_ids)
144 return ctx->ftpuser;
145
146 if (!cache) {
147 cache = RB_tree_new(compare_id, free_payload);
148 setpwent();
149 while ((pw = getpwent()))
150 if (pw->pw_name) {
151 i = Xcalloc(1, sizeof(struct id_item) + strlen(pw->pw_name));
152 i->id = pw->pw_uid;
153 strcpy(i->name, pw->pw_name);
154 RB_insert(cache, i);
155 }
156 endpwent();
157 }
158
159 if (uid == last_uid && last_user)
160 return last_user;
161
162 idi.id = uid;
163
164 if ((t = RB_search(cache, &idi)))
165 u = RB_payload(t, struct id_item *)->name;
166 else {
167 pw = getpwuid(uid);
168 if (pw && pw->pw_name)
169 u = pw->pw_name;
170 if (!u)
171 u = ctx->ftpuser;
172
173 i = Xcalloc(1, sizeof(struct id_item) + strlen(u));
174 i->id = uid;
175 strcpy(i->name, u);
176 RB_insert(cache, i);
177 }
178 last_user = u, last_uid = uid;
179 return u;
180 }
181
lookup_gid(struct context * ctx,gid_t gid)182 char *lookup_gid(struct context *ctx, gid_t gid)
183 {
184 struct group *gr;
185 char *g = NULL;
186 static rb_tree_t *cache = NULL;
187 static uid_t last_gid;
188 static char *last_group = NULL;
189 rb_node_t *t;
190 struct id_item idi, *i;
191
192 if (!ctx->resolve_ids)
193 return ctx->ftpgroup;
194
195 if (!cache) {
196 cache = RB_tree_new(compare_id, free_payload);
197 setgrent();
198 while ((gr = getgrent()))
199 if (gr->gr_name) {
200 i = Xcalloc(1, sizeof(struct id_item) + strlen(gr->gr_name));
201 i->id = gr->gr_gid;
202 strcpy(i->name, gr->gr_name);
203 RB_insert(cache, i);
204 }
205 endgrent();
206 }
207
208 if (gid == last_gid && last_group)
209 return last_group;
210
211 idi.id = gid;
212
213 if ((t = RB_search(cache, &idi)))
214 g = RB_payload(t, struct id_item *)->name;
215 else {
216 gr = getgrgid(gid);
217 if (gr && gr->gr_name)
218 g = gr->gr_name;
219 if (!g)
220 g = ctx->ftpgroup;
221
222 i = Xcalloc(1, sizeof(struct id_item) + strlen(g));
223 i->id = gid;
224 strcpy(i->name, g);
225 RB_insert(cache, i);
226 }
227 last_group = g, last_gid = gid;
228 return g;
229 }
230
231 static char *permtable = NULL;
232
init_permtable(void)233 static char *init_permtable(void)
234 {
235 int i;
236 char *pt[] = { "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx" };
237 char *p;
238
239 permtable = Xcalloc(5120, 1);
240
241 for (p = permtable, i = 0; i < 512; i++, p += 10)
242 sprintf(p, "%s%s%s", pt[i >> 6], pt[7 & (i >> 3)], pt[7 & i]);
243
244 return permtable;
245 }
246
list_one(struct context * ctx,char * filename,enum list_mode mode,char * buffer,size_t buflen)247 static char *list_one(struct context *ctx, char *filename, enum list_mode mode, char *buffer, size_t buflen)
248 {
249 int l;
250 struct stat st;
251 char *t = buffer;
252
253 Debug((DEBUG_PROC, "+ %s(\"%s\", ...\n", __func__, filename));
254
255 if (pickystat(ctx, &st, filename)) {
256 DebugOut(DEBUG_PROC);
257 return NULL;
258 }
259
260 buffer[0] = 0;
261
262 switch (mode) {
263 case List_list:
264 if (!permtable)
265 init_permtable();
266
267 l = snprintf(buffer, buflen,
268 "%c%s %4lu %-8s %-8s %8llu ",
269 S_ISDIR(st.st_mode) ? 'd' : '-',
270 permtable + 10 * (st.st_mode & 0777),
271 (u_long) st.st_nlink, lookup_uid(ctx, st.st_uid), lookup_gid(ctx, st.st_gid), (unsigned long long) st.st_size);
272 strftime(buffer + l, buflen - l, (st.st_mtime + 15552000 < io_now.tv_sec)
273 ? "%b %e %Y " : "%b %e %H:%M ", localtime(&st.st_mtime));
274 break;
275 case List_mlsd:
276 if (!(ctx->mlst_facts & MLST_fact_type) && filename[0] == '.' && (!filename[1] || (filename[1] == '.' && !filename[2])))
277 break;
278 case List_mlst:
279 if (ctx->mlst_facts & MLST_fact_type) {
280 *t++ = 'T';
281 *t++ = 'y';
282 *t++ = 'p';
283 *t++ = 'e';
284 *t++ = '=';
285 if (S_ISDIR(st.st_mode)) {
286 if (filename[0] == '.') {
287 if (filename[1] == '.' && !filename[2])
288 *t++ = 'p';
289 else if (!filename[1])
290 *t++ = 'c';
291 }
292 *t++ = 'd';
293 *t++ = 'i';
294 *t++ = 'r';
295 } else {
296 *t++ = 'f';
297 *t++ = 'i';
298 *t++ = 'l';
299 *t++ = 'e';
300 }
301 *t++ = ';';
302 }
303
304 if (ctx->mlst_facts & MLST_fact_size && !S_ISDIR(st.st_mode))
305 t += snprintf(t, (size_t) (buffer + buflen - t), "Size=%llu;", (unsigned long long) st.st_size);
306
307 if (ctx->mlst_facts & MLST_fact_modify)
308 t += strftime(t, 30, "Modify=%Y%m%d%H%M%S;", gmtime(&st.st_mtime));
309
310 if (ctx->mlst_facts & MLST_fact_change)
311 t += strftime(t, 30, "Change=%Y%m%d%H%M%S;", gmtime(&st.st_ctime));
312
313 if (ctx->mlst_facts & MLST_fact_unique) {
314 char table[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz0123456789+/";
315
316 u_long l1 = (u_long) st.st_dev;
317 u_long l2 = (u_long) st.st_ino;
318
319 *t++ = 'U';
320 *t++ = 'n';
321 *t++ = 'i';
322 *t++ = 'q';
323 *t++ = 'u';
324 *t++ = 'e';
325 *t++ = '=';
326
327 do
328 *t++ = table[l1 & 0x3F];
329 while (l1 >>= 6);
330 *t++ = '.';
331
332 do
333 *t++ = table[l2 & 0x3F];
334 while (l2 >>= 6);
335 *t++ = ';';
336 }
337
338 if (ctx->mlst_facts & MLST_fact_perm) {
339 *t++ = 'P';
340 *t++ = 'e';
341 *t++ = 'r';
342 *t++ = 'm';
343 *t++ = '=';
344
345 if (!S_ISDIR(st.st_mode) && !ctx->anonymous && (ctx->uid == st.st_uid) && (S_IWUSR & st.st_mode))
346 *t++ = 'a'; /* APPE may be applied */
347
348 if (S_ISDIR(st.st_mode)) {
349 if (ctx->anonymous) {
350 if (check_incoming(ctx, filename, 077)) {
351 *t++ = 'c'; /* file creation should succeed */
352 *t++ = 'm'; /* directory creation should succeed */
353 }
354 } else {
355 if ((ctx->uid == st.st_uid) && (S_IWUSR & st.st_mode)) {
356 *t++ = 'c'; /* file creation should succeed */
357 *t++ = 'm'; /* directory creation should succeed */
358 *t++ = 'p'; /* directory contents may be removed */
359 }
360 }
361 }
362
363 if (!ctx->anonymous && ctx->uid == st.st_uid &&
364 (!ctx->pst_valid ||
365 (ctx->pst.st_mode & S_IWUSR) || (ctx->pst.st_mode & S_IWGRP && check_gids(ctx, ctx->pst.st_gid)) || (ctx->pst.st_mode & S_IWOTH))) {
366 *t++ = 'd'; /* rename should succeed */
367 *t++ = 'f'; /* delete should succeed */
368 }
369
370 if (S_ISDIR(st.st_mode) && ((ctx->uid == st.st_uid) || (S_IXGRP & st.st_mode && check_gids(ctx, st.st_gid)) || (S_IXOTH & st.st_mode))) {
371 *t++ = 'e'; /* cwd should succeed */
372 *t++ = 'l'; /* list command may be applied */
373 }
374
375 if (!S_ISDIR(st.st_mode) && ((ctx->uid == st.st_uid) || (S_IRGRP & st.st_mode && check_gids(ctx, st.st_gid)) || (S_IROTH & st.st_mode)))
376 *t++ = 'r'; /* RETR command may be applied */
377
378 if (!S_ISDIR(st.st_mode) && !ctx->anonymous)
379 *t++ = 'w'; /* STOR command may be applied */
380
381 *t++ = ';';
382 }
383
384 if (!S_ISDIR(st.st_mode) && (ctx->mlst_facts & MLST_fact_mediatype && mimetypes)) {
385 char *mt = lookup_mimetype(filename);
386 if (mt)
387 t += snprintf(t, (size_t) (buffer + buflen - t), "Media-Type=%s;", mt);
388 }
389
390 if (ctx->mlst_facts & MLST_fact_UNIX_mode)
391 t += snprintf(t, (size_t) (buffer + buflen - t), "UNIX.mode=%o;", 0777 & (u_int) st.st_mode);
392
393 if (ctx->mlst_facts & MLST_fact_UNIX_owner)
394 t += snprintf(t, (size_t) (buffer + buflen - t), "UNIX.owner=%s;", lookup_uid(ctx, st.st_uid));
395
396 if (ctx->mlst_facts & MLST_fact_UNIX_group)
397 t += snprintf(t, (size_t) (buffer + buflen - t), "UNIX.group=%s;", lookup_gid(ctx, st.st_gid));
398 *t++ = ' ';
399 *t = 0;
400
401 break;
402 case List_nlst:
403 if (nlst_files_only && !S_ISREG(st.st_mode))
404 return NULL;
405 default:
406 buffer[0] = 0;
407 }
408
409 DebugOut(DEBUG_PROC);
410 return buffer;
411 }
412
list_stat(struct context * ctx,char * path)413 void list_stat(struct context *ctx, char *path)
414 {
415 char *t, *u;
416
417 DebugIn(DEBUG_PROC);
418
419 ctx->list_to_cc = 1;
420 ctx->list_mode = List_list;
421 ctx->stat_reply = MSG_550_No_such_file_or_directory;
422
423 if ((t = buildpath(ctx, (path && *path) ? path : "."))) {
424 char buffer[1024];
425 if (!list_dir(ctx, t, NULL)) {
426 replyf(ctx, MSG_212_status_of, path);
427 ctx->stat_reply = MSG_212_status_end;
428 io_sched_add(ctx->io, ctx, (void *) list_dir_details, 0, 0);
429 io_clr_o(ctx->io, ctx->cfn);
430 } else if ((u = list_one(ctx, t, List_list, buffer, sizeof(buffer)))) {
431 replyf(ctx, MSG_213_status_of, path);
432 ctx->stat_reply = MSG_213_status_end;
433 replyf(ctx, "%s%s\r\n", u, list_basename(t));
434 } else if (path && !strchr(path, '/')
435 && !list_dir(ctx, ctx->cwd, path)) {
436 replyf(ctx, MSG_212_status_of, path);
437 ctx->stat_reply = MSG_212_status_end;
438 io_sched_add(ctx->io, ctx, (void *) list_dir_details, 0, 0);
439 io_clr_o(ctx->io, ctx->cfn);
440 }
441 }
442 DebugOut(DEBUG_PROC);
443 }
444
h_mlst(struct context * ctx,char * path)445 void h_mlst(struct context *ctx, char *path)
446 {
447 char *t, *u, buffer[1024];
448
449 DebugIn(DEBUG_PROC);
450
451 t = buildpath(ctx, (path && *path) ? path : ".");
452
453 if (t) {
454 char *v = strrchr(t, '/');
455
456 ctx->pst_valid = 0;
457
458 if (v) {
459 *v = 0;
460 if (!pickystat(ctx, &ctx->pst, t))
461 ctx->pst_valid = 1;
462 }
463
464 if ((u = list_one(ctx, t, List_mlst, buffer, sizeof(buffer)))) {
465 replyf(ctx, MSG_250_listing_start, t[ctx->rootlen] ? t + ctx->rootlen : "/");
466 replyf(ctx, " %s%s\r\n", u, t[ctx->rootlen] ? t + ctx->rootlen : "/");
467 reply(ctx, MSG_250_listing_end);
468 }
469
470 ctx->pst_valid = 0;
471
472 } else
473 reply(ctx, MSG_501_No_such_file_or_directory);
474
475 DebugOut(DEBUG_PROC);
476 }
477
list(struct context * ctx,char * path,enum list_mode mode)478 void list(struct context *ctx, char *path, enum list_mode mode)
479 {
480 char *t, *u;
481
482 DebugIn(DEBUG_PROC);
483
484 ctx->list_to_cc = 0;
485 ctx->buffer_filled = 1;
486 ctx->list_mode = mode;
487
488 if ((t = buildpath(ctx, (path && *path) ? path : ".")))
489 switch (list_dir(ctx, t, NULL)) {
490 case 0:
491 io_sched_add(ctx->io, ctx, (void *) list_dir_details, 0, 0);
492 io_clr_o(ctx->io, ctx->cfn);
493 case EPERM:
494 break;
495 default:
496 if (mode != List_mlsd) {
497 char buffer[1024];
498
499 /* t is a single file */
500 if ((u = list_one(ctx, t, mode, buffer, sizeof(buffer)))) {
501 if (mode != List_nlst)
502 ctx->dbufi = buffer_write(ctx->dbufi, u, strlen(u));
503 t = list_basename(t);
504 ctx->dbufi = buffer_write(ctx->dbufi, t, strlen(t));
505 ctx->dbufi = buffer_write(ctx->dbufi, "\r\n", 2);
506 }
507 /* Workaround for UNIX guys. And for broken clients. */
508 else if (mode != List_mlsd && mode != List_mlst && path && !strchr(path, '/')) {
509 if (path[0] == '-')
510 list(ctx, ".", List_list);
511 else if (!list_dir(ctx, ctx->cwd, path)) {
512 io_sched_add(ctx->io, ctx, (void *) list_dir_details, 0, 0);
513 io_clr_o(ctx->io, ctx->cfn);
514 }
515 }
516 }
517 }
518 DebugOut(DEBUG_PROC);
519 }
520
521 /* list_dir() return values:
522 * 0: OK, Caller should call list_dir_details ()
523 * EINVAL: filter is not a valid globbing expression
524 * EPERM: user has no access to directory
525 * ENOTDIR: dirname is not a directory
526 * ENOENT: dirname does not exist
527 */
528
list_dir(struct context * ctx,char * dirname,char * filter)529 static int list_dir(struct context *ctx, char *dirname, char *filter)
530 {
531 DIR *dir;
532 struct dirent *de;
533 struct glob_pattern *g = NULL;
534
535 Debug((DEBUG_PROC, "+ %s(\"%s\", ...)\n", __func__, dirname));
536
537 if (!dirname)
538 dirname = ".";
539
540 if (filter && !(g = glob_comp(filter))) {
541 Debug((DEBUG_PROC, "- %s: glob_comp failed\n", __func__));
542 return EINVAL;
543 }
544
545 ctx->pst_valid = 0;
546
547 if (pickystat(ctx, &ctx->pst, dirname)) {
548 DebugOut(DEBUG_PROC);
549 return (errno == ENOENT ? ENOENT : EPERM);
550 }
551
552 ctx->pst_valid = 1;
553
554 if (!(dir = opendir(dirname))) {
555 Debug((DEBUG_PROC, "- %s: opendir failure\n", __func__));
556 return (errno == ENOTDIR ? ENOTDIR : EPERM);
557 }
558
559 RB_tree_delete(ctx->filelist);
560 ctx->filelist = RB_tree_new(NULL, free_payload);
561
562 while ((de = readdir(dir))) {
563 char *copy;
564 if (de->d_name[0] == '.') {
565 if (de->d_name[1] == '.' && !de->d_name[2]) {
566 /* don't display ".." in top level root directory */
567 if (ctx->pst.st_ino == ctx->root_ino && ctx->pst.st_dev == ctx->root_dev)
568 continue;
569 } else if (!ctx->allow_dotfiles)
570 continue;
571
572 /* wildcards my not match files starting with a dot */
573 if (filter && filter[0] != '.')
574 continue;
575 }
576
577 if (filter && !glob_exec(g, de->d_name))
578 continue;
579
580 copy = Xstrdup(de->d_name);
581 RB_insert(ctx->filelist, copy);
582 Debug((DEBUG_PROC, "inserted %s\n", copy));
583 }
584
585 if (g)
586 glob_free(g);
587
588 if (RB_empty(ctx->filelist)) {
589 closedir(dir);
590 Debug((DEBUG_PROC, "- %s: no files\n", __func__));
591 return ENOENT;
592 }
593 #ifdef WITH_DIRFD
594 ctx->dirfn = dup(dirfd(dir));
595 closedir(dir);
596 #else /* WITH_DIRFD */
597 closedir(dir);
598 ctx->dirfn = open(dirname, O_RDONLY);
599 #endif /* WITH_DIRFD */
600
601 fcntl(ctx->dirfn, F_SETFD, FD_CLOEXEC);
602
603 DebugOut(DEBUG_PROC);
604 return 0;
605 }
606
list_dir_details(struct context * ctx,int cur)607 static void list_dir_details(struct context *ctx, int cur __attribute__ ((unused)))
608 {
609 rb_node_t *rbn;
610 struct buffer *b;
611 int i = 5;
612 struct buffer *(*bf) (struct buffer *, char *, size_t);
613
614 DebugIn(DEBUG_PROC);
615
616 if ((ctx->uid != (uid_t) - 1) && (current_uid != ctx->uid || current_gid != ctx->gid || update_ids)) {
617 seteuid(0);
618 setgroups(ctx->gids_size, ctx->gids);
619 setegid(ctx->gid);
620 seteuid(ctx->uid);
621 current_gid = ctx->gid;
622 current_uid = ctx->uid;
623 update_ids = 0;
624 }
625
626 if (ctx->dirfn < 0 || fchdir(ctx->dirfn)) {
627 RB_tree_delete(ctx->filelist);
628 ctx->filelist = NULL;
629 if (chdir("/")) {
630 // FIXME
631 }
632 DebugOut(DEBUG_PROC);
633 return;
634 }
635
636 if (ctx->list_to_cc)
637 b = ctx->cbufo, bf = buffer_reply;
638 else
639 b = ctx->dbufi, bf = buffer_write;
640
641 while (i-- && (rbn = RB_first(ctx->filelist))) {
642 char *u, buffer[1024];
643 char *p;
644
645 if ((u = list_one(ctx, p = RB_payload(rbn, char *), ctx->list_mode, buffer, sizeof(buffer))))
646 switch (ctx->list_mode) {
647 case List_mlsd:
648 case List_list:
649 b = bf(b, u, strlen(u));
650 case List_nlst:
651 b = bf(b, p, strlen(p));
652 b = buffer_write(b, "\r\n", 2);
653 default:
654 ;
655 }
656 RB_delete(ctx->filelist, rbn);
657 }
658
659 /* Could use
660 * *(ctx->list_to_cc ? &ctx->cbufo : &ctx->dbufi) = b;
661 * instead of:
662 */
663 if (ctx->list_to_cc)
664 ctx->cbufo = b;
665 else
666 ctx->dbufi = b;
667
668 if (RB_empty(ctx->filelist)) {
669 Debug((DEBUG_PROC, "filelist empty\n"));
670 io_sched_pop(ctx->io, ctx);
671 RB_tree_delete(ctx->filelist);
672 ctx->filelist = NULL;
673 close(ctx->dirfn);
674 ctx->dirfn = -1;
675 ctx->pst_valid = 0;
676 } else
677 io_sched_renew_proc(ctx->io, ctx, (void *) list_dir_details);
678
679 if (chdir("/")) {
680 //FIXME
681 }
682
683 DebugOut(DEBUG_PROC);
684 }
685