1/*-
2 * SPDX-License-Identifier: BSD-3-Clause
3 *
4 * Copyright (c) 1991, 1993, 1994
5 *	The Regents of the University of California.  All rights reserved.
6 *
7 * This code is derived from software contributed to Berkeley by
8 * Steve Hayman of the Computer Science Department, Indiana University,
9 * Michiro Hikida and David Goodenough.
10 *
11 * Redistribution and use in source and binary forms, with or without
12 * modification, are permitted provided that the following conditions
13 * are met:
14 * 1. Redistributions of source code must retain the above copyright
15 *    notice, this list of conditions and the following disclaimer.
16 * 2. Redistributions in binary form must reproduce the above copyright
17 *    notice, this list of conditions and the following disclaimer in the
18 *    documentation and/or other materials provided with the distribution.
19 * 3. Neither the name of the University nor the names of its contributors
20 *    may be used to endorse or promote products derived from this software
21 *    without specific prior written permission.
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
27 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
29 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
32 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33 * SUCH DAMAGE.
34 */
35
36#include <sys/param.h>
37
38#include <err.h>
39#include <errno.h>
40#include <limits.h>
41#include <locale.h>
42#include <stdio.h>
43#include <stdlib.h>
44#include <string.h>
45#include <unistd.h>
46#include <wchar.h>
47
48/*
49 * There's a structure per input file which encapsulates the state of the
50 * file.  We repeatedly read lines from each file until we've read in all
51 * the consecutive lines from the file with a common join field.  Then we
52 * compare the set of lines with an equivalent set from the other file.
53 */
54typedef struct {
55	char *line;		/* line */
56	u_long linealloc;	/* line allocated count */
57	char **fields;		/* line field(s) */
58	u_long fieldcnt;	/* line field(s) count */
59	u_long fieldalloc;	/* line field(s) allocated count */
60} LINE;
61
62typedef struct {
63	FILE *fp;		/* file descriptor */
64	u_long joinf;		/* join field (-1, -2, -j) */
65	int unpair;		/* output unpairable lines (-a) */
66	u_long number;		/* 1 for file 1, 2 for file 2 */
67
68	LINE *set;		/* set of lines with same field */
69	int pushbool;		/* if pushback is set */
70	u_long pushback;	/* line on the stack */
71	u_long setcnt;		/* set count */
72	u_long setalloc;	/* set allocated count */
73} INPUT;
74static INPUT input1 = { NULL, 0, 0, 1, NULL, 0, 0, 0, 0 },
75    input2 = { NULL, 0, 0, 2, NULL, 0, 0, 0, 0 };
76
77typedef struct {
78	u_long	filenum;	/* file number */
79	u_long	fieldno;	/* field number */
80} OLIST;
81static OLIST *olist;		/* output field list */
82static u_long olistcnt;		/* output field list count */
83static u_long olistalloc;	/* output field allocated count */
84
85static int joinout = 1;		/* show lines with matched join fields (-v) */
86static int needsep;		/* need separator character */
87static int spans = 1;		/* span multiple delimiters (-t) */
88static char *empty;		/* empty field replacement string (-e) */
89static wchar_t default_tabchar[] = L" \t";
90static wchar_t *tabchar = default_tabchar; /* delimiter characters (-t) */
91
92static int  cmp(LINE *, u_long, LINE *, u_long);
93static void fieldarg(char *);
94static void joinlines(INPUT *, INPUT *);
95static int  mbscoll(const char *, const char *);
96static char *mbssep(char **, const wchar_t *);
97static void obsolete(char **);
98static void outfield(LINE *, u_long, int);
99static void outoneline(INPUT *, LINE *);
100static void outtwoline(INPUT *, LINE *, INPUT *, LINE *);
101static void slurp(INPUT *);
102static wchar_t *towcs(const char *);
103static void usage(void) __dead2;
104
105int
106main(int argc, char *argv[])
107{
108	INPUT *F1, *F2;
109	int aflag, ch, cval, vflag;
110	char *end;
111
112	setlocale(LC_ALL, "");
113
114	F1 = &input1;
115	F2 = &input2;
116
117	aflag = vflag = 0;
118	obsolete(argv);
119	while ((ch = getopt(argc, argv, "\01a:e:j:1:2:o:t:v:")) != -1) {
120		switch (ch) {
121		case '\01':		/* See comment in obsolete(). */
122			aflag = 1;
123			F1->unpair = F2->unpair = 1;
124			break;
125		case '1':
126			if ((F1->joinf = strtol(optarg, &end, 10)) < 1)
127				errx(1, "-1 option field number less than 1");
128			if (*end)
129				errx(1, "illegal field number -- %s", optarg);
130			--F1->joinf;
131			break;
132		case '2':
133			if ((F2->joinf = strtol(optarg, &end, 10)) < 1)
134				errx(1, "-2 option field number less than 1");
135			if (*end)
136				errx(1, "illegal field number -- %s", optarg);
137			--F2->joinf;
138			break;
139		case 'a':
140			aflag = 1;
141			switch(strtol(optarg, &end, 10)) {
142			case 1:
143				F1->unpair = 1;
144				break;
145			case 2:
146				F2->unpair = 1;
147				break;
148			default:
149				errx(1, "-a option file number not 1 or 2");
150				break;
151			}
152			if (*end)
153				errx(1, "illegal file number -- %s", optarg);
154			break;
155		case 'e':
156			empty = optarg;
157			break;
158		case 'j':
159			if ((F1->joinf = F2->joinf =
160			    strtol(optarg, &end, 10)) < 1)
161				errx(1, "-j option field number less than 1");
162			if (*end)
163				errx(1, "illegal field number -- %s", optarg);
164			--F1->joinf;
165			--F2->joinf;
166			break;
167		case 'o':
168			fieldarg(optarg);
169			break;
170		case 't':
171			spans = 0;
172			if (mbrtowc(&tabchar[0], optarg, MB_LEN_MAX, NULL) !=
173			    strlen(optarg))
174				errx(1, "illegal tab character specification");
175			tabchar[1] = L'\0';
176			break;
177		case 'v':
178			vflag = 1;
179			joinout = 0;
180			switch (strtol(optarg, &end, 10)) {
181			case 1:
182				F1->unpair = 1;
183				break;
184			case 2:
185				F2->unpair = 1;
186				break;
187			default:
188				errx(1, "-v option file number not 1 or 2");
189				break;
190			}
191			if (*end)
192				errx(1, "illegal file number -- %s", optarg);
193			break;
194		case '?':
195		default:
196			usage();
197		}
198	}
199	argc -= optind;
200	argv += optind;
201
202	if (aflag && vflag)
203		errx(1, "the -a and -v options are mutually exclusive");
204
205	if (argc != 2)
206		usage();
207
208	/* Open the files; "-" means stdin. */
209	if (!strcmp(*argv, "-"))
210		F1->fp = stdin;
211	else if ((F1->fp = fopen(*argv, "r")) == NULL)
212		err(1, "%s", *argv);
213	++argv;
214	if (!strcmp(*argv, "-"))
215		F2->fp = stdin;
216	else if ((F2->fp = fopen(*argv, "r")) == NULL)
217		err(1, "%s", *argv);
218	if (F1->fp == stdin && F2->fp == stdin)
219		errx(1, "only one input file may be stdin");
220
221	slurp(F1);
222	slurp(F2);
223	while (F1->setcnt && F2->setcnt) {
224		cval = cmp(F1->set, F1->joinf, F2->set, F2->joinf);
225		if (cval == 0) {
226			/* Oh joy, oh rapture, oh beauty divine! */
227			if (joinout)
228				joinlines(F1, F2);
229			slurp(F1);
230			slurp(F2);
231		} else if (cval < 0) {
232			/* File 1 takes the lead... */
233			if (F1->unpair)
234				joinlines(F1, NULL);
235			slurp(F1);
236		} else {
237			/* File 2 takes the lead... */
238			if (F2->unpair)
239				joinlines(F2, NULL);
240			slurp(F2);
241		}
242	}
243
244	/*
245	 * Now that one of the files is used up, optionally output any
246	 * remaining lines from the other file.
247	 */
248	if (F1->unpair)
249		while (F1->setcnt) {
250			joinlines(F1, NULL);
251			slurp(F1);
252		}
253	if (F2->unpair)
254		while (F2->setcnt) {
255			joinlines(F2, NULL);
256			slurp(F2);
257		}
258	exit(0);
259}
260
261static void
262slurp(INPUT *F)
263{
264	LINE *lp, *lastlp, tmp;
265	size_t blen = 0;
266	ssize_t len;
267	int cnt;
268	char *bp, *buf = NULL, *fieldp;
269
270	/*
271	 * Read all of the lines from an input file that have the same
272	 * join field.
273	 */
274	F->setcnt = 0;
275	for (lastlp = NULL;; ++F->setcnt) {
276		/*
277		 * If we're out of space to hold line structures, allocate
278		 * more.  Initialize the structure so that we know that this
279		 * is new space.
280		 */
281		if (F->setcnt == F->setalloc) {
282			cnt = F->setalloc;
283			F->setalloc += 50;
284			if ((F->set = realloc(F->set,
285			    F->setalloc * sizeof(LINE))) == NULL)
286				err(1, NULL);
287			memset(F->set + cnt, 0, 50 * sizeof(LINE));
288
289			/* re-set lastlp in case it moved */
290			if (lastlp != NULL)
291				lastlp = &F->set[F->setcnt - 1];
292		}
293
294		/*
295		 * Get any pushed back line, else get the next line.  Allocate
296		 * space as necessary.  If taking the line from the stack swap
297		 * the two structures so that we don't lose space allocated to
298		 * either structure.  This could be avoided by doing another
299		 * level of indirection, but it's probably okay as is.
300		 */
301		lp = &F->set[F->setcnt];
302		if (F->setcnt)
303			lastlp = &F->set[F->setcnt - 1];
304		if (F->pushbool) {
305			tmp = F->set[F->setcnt];
306			F->set[F->setcnt] = F->set[F->pushback];
307			F->set[F->pushback] = tmp;
308			F->pushbool = 0;
309			continue;
310		}
311		if ((len = getline(&buf, &blen, F->fp)) < 0) {
312			free(buf);
313			return;
314		}
315		if (lp->linealloc <= (size_t)(len + 1)) {
316			lp->linealloc += MAX(100, len + 1 - lp->linealloc);
317			if ((lp->line =
318			    realloc(lp->line, lp->linealloc)) == NULL)
319				err(1, NULL);
320		}
321		memmove(lp->line, buf, len);
322
323		/* Replace trailing newline, if it exists. */
324		if (buf[len - 1] == '\n')
325			lp->line[len - 1] = '\0';
326		bp = lp->line;
327
328		/* Split the line into fields, allocate space as necessary. */
329		lp->fieldcnt = 0;
330		while ((fieldp = mbssep(&bp, tabchar)) != NULL) {
331			if (spans && *fieldp == '\0')
332				continue;
333			if (lp->fieldcnt == lp->fieldalloc) {
334				lp->fieldalloc += 50;
335				if ((lp->fields = realloc(lp->fields,
336				    lp->fieldalloc * sizeof(char *))) == NULL)
337					err(1, NULL);
338			}
339			lp->fields[lp->fieldcnt++] = fieldp;
340		}
341
342		/* See if the join field value has changed. */
343		if (lastlp != NULL && cmp(lp, F->joinf, lastlp, F->joinf)) {
344			F->pushbool = 1;
345			F->pushback = F->setcnt;
346			break;
347		}
348	}
349	free(buf);
350}
351
352static char *
353mbssep(char **stringp, const wchar_t *delim)
354{
355	char *s, *tok;
356	const wchar_t *spanp;
357	wchar_t c, sc;
358	size_t n;
359
360	if ((s = *stringp) == NULL)
361		return (NULL);
362	for (tok = s;;) {
363		n = mbrtowc(&c, s, MB_LEN_MAX, NULL);
364		if (n == (size_t)-1 || n == (size_t)-2)
365			errc(1, EILSEQ, NULL);	/* XXX */
366		s += n;
367		spanp = delim;
368		do {
369			if ((sc = *spanp++) == c) {
370				if (c == 0)
371					s = NULL;
372				else
373					s[-n] = '\0';
374				*stringp = s;
375				return (tok);
376			}
377		} while (sc != 0);
378	}
379}
380
381static int
382cmp(LINE *lp1, u_long fieldno1, LINE *lp2, u_long fieldno2)
383{
384	if (lp1->fieldcnt <= fieldno1)
385		return (lp2->fieldcnt <= fieldno2 ? 0 : -1);
386	if (lp2->fieldcnt <= fieldno2)
387		return (1);
388	return (mbscoll(lp1->fields[fieldno1], lp2->fields[fieldno2]));
389}
390
391static int
392mbscoll(const char *s1, const char *s2)
393{
394	wchar_t *w1, *w2;
395	int ret;
396
397	if (MB_CUR_MAX == 1)
398		return (strcoll(s1, s2));
399	if ((w1 = towcs(s1)) == NULL || (w2 = towcs(s2)) == NULL)
400		err(1, NULL);	/* XXX */
401	ret = wcscoll(w1, w2);
402	free(w1);
403	free(w2);
404	return (ret);
405}
406
407static wchar_t *
408towcs(const char *s)
409{
410	wchar_t *wcs;
411	size_t n;
412
413	if ((n = mbsrtowcs(NULL, &s, 0, NULL)) == (size_t)-1)
414		return (NULL);
415	if ((wcs = malloc((n + 1) * sizeof(*wcs))) == NULL)
416		return (NULL);
417	mbsrtowcs(wcs, &s, n + 1, NULL);
418	return (wcs);
419}
420
421static void
422joinlines(INPUT *F1, INPUT *F2)
423{
424	u_long cnt1, cnt2;
425
426	/*
427	 * Output the results of a join comparison.  The output may be from
428	 * either file 1 or file 2 (in which case the first argument is the
429	 * file from which to output) or from both.
430	 */
431	if (F2 == NULL) {
432		for (cnt1 = 0; cnt1 < F1->setcnt; ++cnt1)
433			outoneline(F1, &F1->set[cnt1]);
434		return;
435	}
436	for (cnt1 = 0; cnt1 < F1->setcnt; ++cnt1)
437		for (cnt2 = 0; cnt2 < F2->setcnt; ++cnt2)
438			outtwoline(F1, &F1->set[cnt1], F2, &F2->set[cnt2]);
439}
440
441static void
442outoneline(INPUT *F, LINE *lp)
443{
444	u_long cnt;
445
446	/*
447	 * Output a single line from one of the files, according to the
448	 * join rules.  This happens when we are writing unmatched single
449	 * lines.  Output empty fields in the right places.
450	 */
451	if (olist)
452		for (cnt = 0; cnt < olistcnt; ++cnt) {
453			if (olist[cnt].filenum == (unsigned)F->number)
454				outfield(lp, olist[cnt].fieldno, 0);
455			else if (olist[cnt].filenum == 0)
456				outfield(lp, F->joinf, 0);
457			else
458				outfield(lp, 0, 1);
459		}
460	else {
461		/*
462		 * Output the join field, then the remaining fields.
463		 */
464		outfield(lp, F->joinf, 0);
465		for (cnt = 0; cnt < lp->fieldcnt; ++cnt)
466			if (F->joinf != cnt)
467				outfield(lp, cnt, 0);
468	}
469	(void)printf("\n");
470	if (ferror(stdout))
471		err(1, "stdout");
472	needsep = 0;
473}
474
475static void
476outtwoline(INPUT *F1, LINE *lp1, INPUT *F2, LINE *lp2)
477{
478	u_long cnt;
479
480	/* Output a pair of lines according to the join list (if any). */
481	if (olist)
482		for (cnt = 0; cnt < olistcnt; ++cnt)
483			if (olist[cnt].filenum == 0) {
484				if (lp1->fieldcnt >= F1->joinf)
485					outfield(lp1, F1->joinf, 0);
486				else
487					outfield(lp2, F2->joinf, 0);
488			} else if (olist[cnt].filenum == 1)
489				outfield(lp1, olist[cnt].fieldno, 0);
490			else /* if (olist[cnt].filenum == 2) */
491				outfield(lp2, olist[cnt].fieldno, 0);
492	else {
493		/*
494		 * Output the join field, then the remaining fields from F1
495		 * and F2.
496		 */
497		outfield(lp1, F1->joinf, 0);
498		for (cnt = 0; cnt < lp1->fieldcnt; ++cnt)
499			if (F1->joinf != cnt)
500				outfield(lp1, cnt, 0);
501		for (cnt = 0; cnt < lp2->fieldcnt; ++cnt)
502			if (F2->joinf != cnt)
503				outfield(lp2, cnt, 0);
504	}
505	(void)printf("\n");
506	if (ferror(stdout))
507		err(1, "stdout");
508	needsep = 0;
509}
510
511static void
512outfield(LINE *lp, u_long fieldno, int out_empty)
513{
514	if (needsep++)
515		(void)printf("%lc", (wint_t)*tabchar);
516	if (!ferror(stdout)) {
517		if (lp->fieldcnt <= fieldno || out_empty) {
518			if (empty != NULL)
519				(void)printf("%s", empty);
520		} else {
521			if (*lp->fields[fieldno] == '\0')
522				return;
523			(void)printf("%s", lp->fields[fieldno]);
524		}
525	}
526	if (ferror(stdout))
527		err(1, "stdout");
528}
529
530/*
531 * Convert an output list argument "2.1, 1.3, 2.4" into an array of output
532 * fields.
533 */
534static void
535fieldarg(char *option)
536{
537	u_long fieldno, filenum;
538	char *end, *token;
539
540	while ((token = strsep(&option, ", \t")) != NULL) {
541		if (*token == '\0')
542			continue;
543		if (token[0] == '0')
544			filenum = fieldno = 0;
545		else if ((token[0] == '1' || token[0] == '2') &&
546		    token[1] == '.') {
547			filenum = token[0] - '0';
548			fieldno = strtol(token + 2, &end, 10);
549			if (*end)
550				errx(1, "malformed -o option field");
551			if (fieldno == 0)
552				errx(1, "field numbers are 1 based");
553			--fieldno;
554		} else
555			errx(1, "malformed -o option field");
556		if (olistcnt == olistalloc) {
557			olistalloc += 50;
558			if ((olist = realloc(olist,
559			    olistalloc * sizeof(OLIST))) == NULL)
560				err(1, NULL);
561		}
562		olist[olistcnt].filenum = filenum;
563		olist[olistcnt].fieldno = fieldno;
564		++olistcnt;
565	}
566}
567
568static void
569obsolete(char **argv)
570{
571	size_t len;
572	char **p, *ap, *t;
573
574	while ((ap = *++argv) != NULL) {
575		/* Return if "--". */
576		if (ap[0] == '-' && ap[1] == '-')
577			return;
578		/* skip if not an option */
579		if (ap[0] != '-')
580			continue;
581		switch (ap[1]) {
582		case 'a':
583			/*
584			 * The original join allowed "-a", which meant the
585			 * same as -a1 plus -a2.  POSIX 1003.2, Draft 11.2
586			 * only specifies this as "-a 1" and "a -2", so we
587			 * have to use another option flag, one that is
588			 * unlikely to ever be used or accidentally entered
589			 * on the command line.  (Well, we could reallocate
590			 * the argv array, but that hardly seems worthwhile.)
591			 */
592			if (ap[2] == '\0' && (argv[1] == NULL ||
593			    (strcmp(argv[1], "1") != 0 &&
594			    strcmp(argv[1], "2") != 0))) {
595				ap[1] = '\01';
596				warnx("-a option used without an argument; "
597				    "reverting to historical behavior");
598			}
599			break;
600		case 'j':
601			/*
602			 * The original join allowed "-j[12] arg" and "-j arg".
603			 * Convert the former to "-[12] arg".  Don't convert
604			 * the latter since getopt(3) can handle it.
605			 */
606			switch(ap[2]) {
607			case '1':
608				if (ap[3] != '\0')
609					goto jbad;
610				ap[1] = '1';
611				ap[2] = '\0';
612				break;
613			case '2':
614				if (ap[3] != '\0')
615					goto jbad;
616				ap[1] = '2';
617				ap[2] = '\0';
618				break;
619			case '\0':
620				break;
621			default:
622jbad:				errx(1, "illegal option -- %s", ap);
623				usage();
624			}
625			break;
626		case 'o':
627			/*
628			 * The original join allowed "-o arg arg".
629			 * Convert to "-o arg -o arg".
630			 */
631			if (ap[2] != '\0')
632				break;
633			for (p = argv + 2; *p; ++p) {
634				if (p[0][0] == '0' || ((p[0][0] != '1' &&
635				    p[0][0] != '2') || p[0][1] != '.'))
636					break;
637				len = strlen(*p);
638				if (len - 2 != strspn(*p + 2, "0123456789"))
639					break;
640				if ((t = malloc(len + 3)) == NULL)
641					err(1, NULL);
642				t[0] = '-';
643				t[1] = 'o';
644				memmove(t + 2, *p, len + 1);
645				*p = t;
646			}
647			argv = p - 1;
648			break;
649		}
650	}
651}
652
653static void
654usage(void)
655{
656	(void)fprintf(stderr, "%s %s\n%s\n",
657	    "usage: join [-a fileno | -v fileno ] [-e string] [-1 field]",
658	    "[-2 field]",
659		"            [-o list] [-t char] file1 file2");
660	exit(1);
661}
662