xref: /original-bsd/libexec/ftpd/ftpcmd.y (revision 57530171)
1 /*
2  * Copyright (c) 1985 Regents of the University of California.
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms are permitted
6  * provided that this notice is preserved and that due credit is given
7  * to the University of California at Berkeley. The name of the University
8  * may not be used to endorse or promote products derived from this
9  * software without specific prior written permission. This software
10  * is provided ``as is'' without express or implied warranty.
11  */
12 
13 /*
14  * Grammar for FTP commands.
15  * See RFC 765.
16  */
17 
18 %{
19 
20 #ifndef lint
21 static char sccsid[] = "@(#)ftpcmd.y	5.10 (Berkeley) 03/14/88";
22 #endif /* not lint */
23 
24 #include <sys/types.h>
25 #include <sys/socket.h>
26 
27 #include <netinet/in.h>
28 
29 #include <arpa/ftp.h>
30 
31 #include <stdio.h>
32 #include <signal.h>
33 #include <ctype.h>
34 #include <pwd.h>
35 #include <setjmp.h>
36 #include <syslog.h>
37 
38 extern	struct sockaddr_in data_dest;
39 extern	int logged_in;
40 extern	struct passwd *pw;
41 extern	int guest;
42 extern	int logging;
43 extern	int type;
44 extern	int form;
45 extern	int debug;
46 extern	int timeout;
47 extern  int pdata;
48 extern	char hostname[];
49 extern	char *globerr;
50 extern	int usedefault;
51 extern	int unique;
52 extern  int transflag;
53 extern  char tmpline[];
54 char	**glob();
55 
56 static	int cmd_type;
57 static	int cmd_form;
58 static	int cmd_bytesz;
59 char cbuf[512];
60 char *fromname;
61 
62 char	*index();
63 %}
64 
65 %token
66 	A	B	C	E	F	I
67 	L	N	P	R	S	T
68 
69 	SP	CRLF	COMMA	STRING	NUMBER
70 
71 	USER	PASS	ACCT	REIN	QUIT	PORT
72 	PASV	TYPE	STRU	MODE	RETR	STOR
73 	APPE	MLFL	MAIL	MSND	MSOM	MSAM
74 	MRSQ	MRCP	ALLO	REST	RNFR	RNTO
75 	ABOR	DELE	CWD	LIST	NLST	SITE
76 	STAT	HELP	NOOP	XMKD	XRMD	XPWD
77 	XCUP	STOU
78 
79 	LEXERR
80 
81 %start	cmd_list
82 
83 %%
84 
85 cmd_list:	/* empty */
86 	|	cmd_list cmd
87 		= {
88 			fromname = (char *) 0;
89 		}
90 	|	cmd_list rcmd
91 	;
92 
93 cmd:		USER SP username CRLF
94 		= {
95 			extern struct passwd *getpwnam();
96 
97 			logged_in = 0;
98 			if (strcmp((char *) $3, "ftp") == 0 ||
99 			  strcmp((char *) $3, "anonymous") == 0) {
100 				if ((pw = getpwnam("ftp")) != NULL) {
101 					guest = 1;
102 					reply(331,
103 				  "Guest login ok, send ident as password.");
104 				}
105 				else {
106 					reply(530, "User %s unknown.", $3);
107 				}
108 			} else if (checkuser((char *) $3)) {
109 				guest = 0;
110 				pw = getpwnam((char *) $3);
111 				if (pw == NULL) {
112 					reply(530, "User %s unknown.", $3);
113 				}
114 				else {
115 				    reply(331, "Password required for %s.", $3);
116 				}
117 			} else {
118 				reply(530, "User %s access denied.", $3);
119 			}
120 			free((char *) $3);
121 		}
122 	|	PASS SP password CRLF
123 		= {
124 			pass((char *) $3);
125 			free((char *) $3);
126 		}
127 	|	PORT SP host_port CRLF
128 		= {
129 			usedefault = 0;
130 			if (pdata > 0) {
131 				(void) close(pdata);
132 			}
133 			pdata = -1;
134 			reply(200, "PORT command successful.");
135 		}
136 	|	PASV CRLF
137 		= {
138 			passive();
139 		}
140 	|	TYPE SP type_code CRLF
141 		= {
142 			switch (cmd_type) {
143 
144 			case TYPE_A:
145 				if (cmd_form == FORM_N) {
146 					reply(200, "Type set to A.");
147 					type = cmd_type;
148 					form = cmd_form;
149 				} else
150 					reply(504, "Form must be N.");
151 				break;
152 
153 			case TYPE_E:
154 				reply(504, "Type E not implemented.");
155 				break;
156 
157 			case TYPE_I:
158 				reply(200, "Type set to I.");
159 				type = cmd_type;
160 				break;
161 
162 			case TYPE_L:
163 				if (cmd_bytesz == 8) {
164 					reply(200,
165 					    "Type set to L (byte size 8).");
166 					type = cmd_type;
167 				} else
168 					reply(504, "Byte size must be 8.");
169 			}
170 		}
171 	|	STRU SP struct_code CRLF
172 		= {
173 			switch ($3) {
174 
175 			case STRU_F:
176 				reply(200, "STRU F ok.");
177 				break;
178 
179 			default:
180 				reply(504, "Unimplemented STRU type.");
181 			}
182 		}
183 	|	MODE SP mode_code CRLF
184 		= {
185 			switch ($3) {
186 
187 			case MODE_S:
188 				reply(200, "MODE S ok.");
189 				break;
190 
191 			default:
192 				reply(502, "Unimplemented MODE type.");
193 			}
194 		}
195 	|	ALLO SP NUMBER CRLF
196 		= {
197 			reply(202, "ALLO command ignored.");
198 		}
199 	|	RETR check_login SP pathname CRLF
200 		= {
201 			if ($2 && $4 != NULL)
202 				retrieve((char *) 0, (char *) $4);
203 			if ($4 != NULL)
204 				free((char *) $4);
205 		}
206 	|	STOR check_login SP pathname CRLF
207 		= {
208 			if ($2 && $4 != NULL)
209 				store((char *) $4, "w");
210 			if ($4 != NULL)
211 				free((char *) $4);
212 		}
213 	|	APPE check_login SP pathname CRLF
214 		= {
215 			if ($2 && $4 != NULL)
216 				store((char *) $4, "a");
217 			if ($4 != NULL)
218 				free((char *) $4);
219 		}
220 	|	NLST check_login CRLF
221 		= {
222 			if ($2)
223 				retrieve("/bin/ls", "");
224 		}
225 	|	NLST check_login SP pathname CRLF
226 		= {
227 			if ($2 && $4 != NULL)
228 				retrieve("/bin/ls %s", (char *) $4);
229 			if ($4 != NULL)
230 				free((char *) $4);
231 		}
232 	|	LIST check_login CRLF
233 		= {
234 			if ($2)
235 				retrieve("/bin/ls -lg", "");
236 		}
237 	|	LIST check_login SP pathname CRLF
238 		= {
239 			if ($2 && $4 != NULL)
240 				retrieve("/bin/ls -lg %s", (char *) $4);
241 			if ($4 != NULL)
242 				free((char *) $4);
243 		}
244 	|	DELE check_login SP pathname CRLF
245 		= {
246 			if ($2 && $4 != NULL)
247 				delete((char *) $4);
248 			if ($4 != NULL)
249 				free((char *) $4);
250 		}
251 	|	RNTO SP pathname CRLF
252 		= {
253 			if (fromname) {
254 				renamecmd(fromname, (char *) $3);
255 				free(fromname);
256 				fromname = (char *) 0;
257 			} else {
258 				reply(503, "Bad sequence of commands.");
259 			}
260 			free((char *) $3);
261 		}
262 	|	ABOR CRLF
263 		= {
264 			reply(225, "ABOR command successful.");
265 		}
266 	|	CWD check_login CRLF
267 		= {
268 			if ($2)
269 				cwd(pw->pw_dir);
270 		}
271 	|	CWD check_login SP pathname CRLF
272 		= {
273 			if ($2 && $4 != NULL)
274 				cwd((char *) $4);
275 			if ($4 != NULL)
276 				free((char *) $4);
277 		}
278 	|	HELP CRLF
279 		= {
280 			help((char *) 0);
281 		}
282 	|	HELP SP STRING CRLF
283 		= {
284 			help((char *) $3);
285 		}
286 	|	NOOP CRLF
287 		= {
288 			reply(200, "NOOP command successful.");
289 		}
290 	|	XMKD check_login SP pathname CRLF
291 		= {
292 			if ($2 && $4 != NULL)
293 				makedir((char *) $4);
294 			if ($4 != NULL)
295 				free((char *) $4);
296 		}
297 	|	XRMD check_login SP pathname CRLF
298 		= {
299 			if ($2 && $4 != NULL)
300 				removedir((char *) $4);
301 			if ($4 != NULL)
302 				free((char *) $4);
303 		}
304 	|	XPWD check_login CRLF
305 		= {
306 			if ($2)
307 				pwd();
308 		}
309 	|	XCUP check_login CRLF
310 		= {
311 			if ($2)
312 				cwd("..");
313 		}
314 	|	STOU check_login SP pathname CRLF
315 		= {
316 			if ($2 && $4 != NULL) {
317 				unique++;
318 				store((char *) $4, "w");
319 				unique = 0;
320 			}
321 			if ($4 != NULL)
322 				free((char *) $4);
323 		}
324 	|	QUIT CRLF
325 		= {
326 			reply(221, "Goodbye.");
327 			dologout(0);
328 		}
329 	|	error CRLF
330 		= {
331 			yyerrok;
332 		}
333 	;
334 
335 rcmd:		RNFR check_login SP pathname CRLF
336 		= {
337 			char *renamefrom();
338 
339 			if ($2 && $4) {
340 				fromname = renamefrom((char *) $4);
341 				if (fromname == (char *) 0 && $4) {
342 					free((char *) $4);
343 				}
344 			}
345 		}
346 	;
347 
348 username:	STRING
349 	;
350 
351 password:	STRING
352 	;
353 
354 byte_size:	NUMBER
355 	;
356 
357 host_port:	NUMBER COMMA NUMBER COMMA NUMBER COMMA NUMBER COMMA
358 		NUMBER COMMA NUMBER
359 		= {
360 			register char *a, *p;
361 
362 			a = (char *)&data_dest.sin_addr;
363 			a[0] = $1; a[1] = $3; a[2] = $5; a[3] = $7;
364 			p = (char *)&data_dest.sin_port;
365 			p[0] = $9; p[1] = $11;
366 			data_dest.sin_family = AF_INET;
367 		}
368 	;
369 
370 form_code:	N
371 	= {
372 		$$ = FORM_N;
373 	}
374 	|	T
375 	= {
376 		$$ = FORM_T;
377 	}
378 	|	C
379 	= {
380 		$$ = FORM_C;
381 	}
382 	;
383 
384 type_code:	A
385 	= {
386 		cmd_type = TYPE_A;
387 		cmd_form = FORM_N;
388 	}
389 	|	A SP form_code
390 	= {
391 		cmd_type = TYPE_A;
392 		cmd_form = $3;
393 	}
394 	|	E
395 	= {
396 		cmd_type = TYPE_E;
397 		cmd_form = FORM_N;
398 	}
399 	|	E SP form_code
400 	= {
401 		cmd_type = TYPE_E;
402 		cmd_form = $3;
403 	}
404 	|	I
405 	= {
406 		cmd_type = TYPE_I;
407 	}
408 	|	L
409 	= {
410 		cmd_type = TYPE_L;
411 		cmd_bytesz = 8;
412 	}
413 	|	L SP byte_size
414 	= {
415 		cmd_type = TYPE_L;
416 		cmd_bytesz = $3;
417 	}
418 	/* this is for a bug in the BBN ftp */
419 	|	L byte_size
420 	= {
421 		cmd_type = TYPE_L;
422 		cmd_bytesz = $2;
423 	}
424 	;
425 
426 struct_code:	F
427 	= {
428 		$$ = STRU_F;
429 	}
430 	|	R
431 	= {
432 		$$ = STRU_R;
433 	}
434 	|	P
435 	= {
436 		$$ = STRU_P;
437 	}
438 	;
439 
440 mode_code:	S
441 	= {
442 		$$ = MODE_S;
443 	}
444 	|	B
445 	= {
446 		$$ = MODE_B;
447 	}
448 	|	C
449 	= {
450 		$$ = MODE_C;
451 	}
452 	;
453 
454 pathname:	pathstring
455 	= {
456 		/*
457 		 * Problem: this production is used for all pathname
458 		 * processing, but only gives a 550 error reply.
459 		 * This is a valid reply in some cases but not in others.
460 		 */
461 		if ($1 && strncmp((char *) $1, "~", 1) == 0) {
462 			$$ = (int)*glob((char *) $1);
463 			if (globerr != NULL) {
464 				reply(550, globerr);
465 				$$ = NULL;
466 			}
467 			free((char *) $1);
468 		} else
469 			$$ = $1;
470 	}
471 	;
472 
473 pathstring:	STRING
474 	;
475 
476 check_login:	/* empty */
477 	= {
478 		if (logged_in)
479 			$$ = 1;
480 		else {
481 			reply(530, "Please login with USER and PASS.");
482 			$$ = 0;
483 		}
484 	}
485 	;
486 
487 %%
488 
489 extern jmp_buf errcatch;
490 
491 #define	CMD	0	/* beginning of command */
492 #define	ARGS	1	/* expect miscellaneous arguments */
493 #define	STR1	2	/* expect SP followed by STRING */
494 #define	STR2	3	/* expect STRING */
495 #define	OSTR	4	/* optional STRING */
496 
497 struct tab {
498 	char	*name;
499 	short	token;
500 	short	state;
501 	short	implemented;	/* 1 if command is implemented */
502 	char	*help;
503 };
504 
505 struct tab cmdtab[] = {		/* In order defined in RFC 765 */
506 	{ "USER", USER, STR1, 1,	"<sp> username" },
507 	{ "PASS", PASS, STR1, 1,	"<sp> password" },
508 	{ "ACCT", ACCT, STR1, 0,	"(specify account)" },
509 	{ "REIN", REIN, ARGS, 0,	"(reinitialize server state)" },
510 	{ "QUIT", QUIT, ARGS, 1,	"(terminate service)", },
511 	{ "PORT", PORT, ARGS, 1,	"<sp> b0, b1, b2, b3, b4" },
512 	{ "PASV", PASV, ARGS, 1,	"(set server in passive mode)" },
513 	{ "TYPE", TYPE, ARGS, 1,	"<sp> [ A | E | I | L ]" },
514 	{ "STRU", STRU, ARGS, 1,	"(specify file structure)" },
515 	{ "MODE", MODE, ARGS, 1,	"(specify transfer mode)" },
516 	{ "RETR", RETR, STR1, 1,	"<sp> file-name" },
517 	{ "STOR", STOR, STR1, 1,	"<sp> file-name" },
518 	{ "APPE", APPE, STR1, 1,	"<sp> file-name" },
519 	{ "MLFL", MLFL, OSTR, 0,	"(mail file)" },
520 	{ "MAIL", MAIL, OSTR, 0,	"(mail to user)" },
521 	{ "MSND", MSND, OSTR, 0,	"(mail send to terminal)" },
522 	{ "MSOM", MSOM, OSTR, 0,	"(mail send to terminal or mailbox)" },
523 	{ "MSAM", MSAM, OSTR, 0,	"(mail send to terminal and mailbox)" },
524 	{ "MRSQ", MRSQ, OSTR, 0,	"(mail recipient scheme question)" },
525 	{ "MRCP", MRCP, STR1, 0,	"(mail recipient)" },
526 	{ "ALLO", ALLO, ARGS, 1,	"allocate storage (vacuously)" },
527 	{ "REST", REST, STR1, 0,	"(restart command)" },
528 	{ "RNFR", RNFR, STR1, 1,	"<sp> file-name" },
529 	{ "RNTO", RNTO, STR1, 1,	"<sp> file-name" },
530 	{ "ABOR", ABOR, ARGS, 1,	"(abort operation)" },
531 	{ "DELE", DELE, STR1, 1,	"<sp> file-name" },
532 	{ "CWD",  CWD,  OSTR, 1,	"[ <sp> directory-name]" },
533 	{ "XCWD", CWD,	OSTR, 1,	"[ <sp> directory-name ]" },
534 	{ "LIST", LIST, OSTR, 1,	"[ <sp> path-name ]" },
535 	{ "NLST", NLST, OSTR, 1,	"[ <sp> path-name ]" },
536 	{ "SITE", SITE, STR1, 0,	"(get site parameters)" },
537 	{ "STAT", STAT, OSTR, 0,	"(get server status)" },
538 	{ "HELP", HELP, OSTR, 1,	"[ <sp> <string> ]" },
539 	{ "NOOP", NOOP, ARGS, 1,	"" },
540 	{ "MKD",  XMKD, STR1, 1,	"<sp> path-name" },
541 	{ "XMKD", XMKD, STR1, 1,	"<sp> path-name" },
542 	{ "RMD",  XRMD, STR1, 1,	"<sp> path-name" },
543 	{ "XRMD", XRMD, STR1, 1,	"<sp> path-name" },
544 	{ "PWD",  XPWD, ARGS, 1,	"(return current directory)" },
545 	{ "XPWD", XPWD, ARGS, 1,	"(return current directory)" },
546 	{ "CDUP", XCUP, ARGS, 1,	"(change to parent directory)" },
547 	{ "XCUP", XCUP, ARGS, 1,	"(change to parent directory)" },
548 	{ "STOU", STOU, STR1, 1,	"<sp> file-name" },
549 	{ NULL,   0,    0,    0,	0 }
550 };
551 
552 struct tab *
553 lookup(cmd)
554 	char *cmd;
555 {
556 	register struct tab *p;
557 
558 	for (p = cmdtab; p->name != NULL; p++)
559 		if (strcmp(cmd, p->name) == 0)
560 			return (p);
561 	return (0);
562 }
563 
564 #include <arpa/telnet.h>
565 
566 /*
567  * getline - a hacked up version of fgets to ignore TELNET escape codes.
568  */
569 char *
570 getline(s, n, iop)
571 	char *s;
572 	register FILE *iop;
573 {
574 	register c;
575 	register char *cs;
576 
577 	cs = s;
578 /* tmpline may contain saved command from urgent mode interruption */
579 	for (c = 0; tmpline[c] != '\0' && --n > 0; ++c) {
580 		*cs++ = tmpline[c];
581 		if (tmpline[c] == '\n') {
582 			*cs++ = '\0';
583 			if (debug) {
584 				syslog(LOG_DEBUG, "FTPD: command: %s", s);
585 			}
586 			tmpline[0] = '\0';
587 			return(s);
588 		}
589 		if (c == 0) {
590 			tmpline[0] = '\0';
591 		}
592 	}
593 	while (--n > 0 && (c = getc(iop)) != EOF) {
594 		c = 0377 & c;
595 		while (c == IAC) {
596 			switch (c = 0377 & getc(iop)) {
597 			case WILL:
598 			case WONT:
599 				c = 0377 & getc(iop);
600 				printf("%c%c%c", IAC, WONT, c);
601 				(void) fflush(stdout);
602 				break;
603 			case DO:
604 			case DONT:
605 				c = 0377 & getc(iop);
606 				printf("%c%c%c", IAC, DONT, c);
607 				(void) fflush(stdout);
608 				break;
609 			default:
610 				break;
611 			}
612 			c = 0377 & getc(iop); /* try next character */
613 		}
614 		*cs++ = c;
615 		if (c=='\n')
616 			break;
617 	}
618 	if (c == EOF && cs == s)
619 		return (NULL);
620 	*cs++ = '\0';
621 	if (debug) {
622 		syslog(LOG_DEBUG, "FTPD: command: %s", s);
623 	}
624 	return (s);
625 }
626 
627 static int
628 toolong()
629 {
630 	time_t now;
631 	extern char *ctime();
632 	extern time_t time();
633 
634 	reply(421,
635 	  "Timeout (%d seconds): closing control connection.", timeout);
636 	(void) time(&now);
637 	if (logging) {
638 		syslog(LOG_INFO,
639 			"FTPD: User %s timed out after %d seconds at %s",
640 			(pw ? pw -> pw_name : "unknown"), timeout, ctime(&now));
641 	}
642 	dologout(1);
643 }
644 
645 yylex()
646 {
647 	static int cpos, state;
648 	register char *cp;
649 	register struct tab *p;
650 	int n;
651 	char c;
652 
653 	for (;;) {
654 		switch (state) {
655 
656 		case CMD:
657 			(void) signal(SIGALRM, toolong);
658 			(void) alarm((unsigned) timeout);
659 			if (getline(cbuf, sizeof(cbuf)-1, stdin) == NULL) {
660 				reply(221, "You could at least say goodbye.");
661 				dologout(0);
662 			}
663 			(void) alarm(0);
664 			if (index(cbuf, '\r')) {
665 				cp = index(cbuf, '\r');
666 				cp[0] = '\n'; cp[1] = 0;
667 			}
668 			if (index(cbuf, ' '))
669 				cpos = index(cbuf, ' ') - cbuf;
670 			else
671 				cpos = index(cbuf, '\n') - cbuf;
672 			if (cpos == 0) {
673 				cpos = 4;
674 			}
675 			c = cbuf[cpos];
676 			cbuf[cpos] = '\0';
677 			upper(cbuf);
678 			p = lookup(cbuf);
679 			cbuf[cpos] = c;
680 			if (p != 0) {
681 				if (p->implemented == 0) {
682 					nack(p->name);
683 					longjmp(errcatch,0);
684 					/* NOTREACHED */
685 				}
686 				state = p->state;
687 				yylval = (int) p->name;
688 				return (p->token);
689 			}
690 			break;
691 
692 		case OSTR:
693 			if (cbuf[cpos] == '\n') {
694 				state = CMD;
695 				return (CRLF);
696 			}
697 			/* FALL THRU */
698 
699 		case STR1:
700 			if (cbuf[cpos] == ' ') {
701 				cpos++;
702 				state = STR2;
703 				return (SP);
704 			}
705 			break;
706 
707 		case STR2:
708 			cp = &cbuf[cpos];
709 			n = strlen(cp);
710 			cpos += n - 1;
711 			/*
712 			 * Make sure the string is nonempty and \n terminated.
713 			 */
714 			if (n > 1 && cbuf[cpos] == '\n') {
715 				cbuf[cpos] = '\0';
716 				yylval = copy(cp);
717 				cbuf[cpos] = '\n';
718 				state = ARGS;
719 				return (STRING);
720 			}
721 			break;
722 
723 		case ARGS:
724 			if (isdigit(cbuf[cpos])) {
725 				cp = &cbuf[cpos];
726 				while (isdigit(cbuf[++cpos]))
727 					;
728 				c = cbuf[cpos];
729 				cbuf[cpos] = '\0';
730 				yylval = atoi(cp);
731 				cbuf[cpos] = c;
732 				return (NUMBER);
733 			}
734 			switch (cbuf[cpos++]) {
735 
736 			case '\n':
737 				state = CMD;
738 				return (CRLF);
739 
740 			case ' ':
741 				return (SP);
742 
743 			case ',':
744 				return (COMMA);
745 
746 			case 'A':
747 			case 'a':
748 				return (A);
749 
750 			case 'B':
751 			case 'b':
752 				return (B);
753 
754 			case 'C':
755 			case 'c':
756 				return (C);
757 
758 			case 'E':
759 			case 'e':
760 				return (E);
761 
762 			case 'F':
763 			case 'f':
764 				return (F);
765 
766 			case 'I':
767 			case 'i':
768 				return (I);
769 
770 			case 'L':
771 			case 'l':
772 				return (L);
773 
774 			case 'N':
775 			case 'n':
776 				return (N);
777 
778 			case 'P':
779 			case 'p':
780 				return (P);
781 
782 			case 'R':
783 			case 'r':
784 				return (R);
785 
786 			case 'S':
787 			case 's':
788 				return (S);
789 
790 			case 'T':
791 			case 't':
792 				return (T);
793 
794 			}
795 			break;
796 
797 		default:
798 			fatal("Unknown state in scanner.");
799 		}
800 		yyerror((char *) 0);
801 		state = CMD;
802 		longjmp(errcatch,0);
803 	}
804 }
805 
806 upper(s)
807 	char *s;
808 {
809 	while (*s != '\0') {
810 		if (islower(*s))
811 			*s = toupper(*s);
812 		s++;
813 	}
814 }
815 
816 copy(s)
817 	char *s;
818 {
819 	char *p;
820 	extern char *malloc(), *strcpy();
821 
822 	p = malloc((unsigned) strlen(s) + 1);
823 	if (p == NULL)
824 		fatal("Ran out of memory.");
825 	(void) strcpy(p, s);
826 	return ((int)p);
827 }
828 
829 help(s)
830 	char *s;
831 {
832 	register struct tab *c;
833 	register int width, NCMDS;
834 
835 	width = 0, NCMDS = 0;
836 	for (c = cmdtab; c->name != NULL; c++) {
837 		int len = strlen(c->name) + 1;
838 
839 		if (len > width)
840 			width = len;
841 		NCMDS++;
842 	}
843 	width = (width + 8) &~ 7;
844 	if (s == 0) {
845 		register int i, j, w;
846 		int columns, lines;
847 
848 		lreply(214,
849 	  "The following commands are recognized (* =>'s unimplemented).");
850 		columns = 76 / width;
851 		if (columns == 0)
852 			columns = 1;
853 		lines = (NCMDS + columns - 1) / columns;
854 		for (i = 0; i < lines; i++) {
855 			printf("   ");
856 			for (j = 0; j < columns; j++) {
857 				c = cmdtab + j * lines + i;
858 				printf("%s%c", c->name,
859 					c->implemented ? ' ' : '*');
860 				if (c + lines >= &cmdtab[NCMDS])
861 					break;
862 				w = strlen(c->name) + 1;
863 				while (w < width) {
864 					putchar(' ');
865 					w++;
866 				}
867 			}
868 			printf("\r\n");
869 		}
870 		(void) fflush(stdout);
871 		reply(214, "Direct comments to ftp-bugs@%s.", hostname);
872 		return;
873 	}
874 	upper(s);
875 	c = lookup(s);
876 	if (c == (struct tab *)0) {
877 		reply(502, "Unknown command %s.", s);
878 		return;
879 	}
880 	if (c->implemented)
881 		reply(214, "Syntax: %s %s", c->name, c->help);
882 	else
883 		reply(214, "%-*s\t%s; unimplemented.", width, c->name, c->help);
884 }
885